imip-agent

imiptools/period.py

962:ecd915425718
2015-10-29 Paul Boddie Check corrected periods against the free/busy schedules of all attendees. Used the coalescing of busy periods from attendees followed by the inversion of the result to get the available free periods.
     1 #!/usr/bin/env python     2      3 """     4 Managing and presenting periods of time.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from bisect import bisect_left, bisect_right, insort_left    23 from datetime import date, datetime, timedelta    24 from imiptools.dates import check_permitted_values, correct_datetime, \    25                             format_datetime, get_datetime, \    26                             get_datetime_attributes, \    27                             get_recurrence_start, get_recurrence_start_point, \    28                             get_start_of_day, \    29                             get_tzid, \    30                             to_timezone, to_utc_datetime    31     32 def ifnone(x, y):    33     if x is None: return y    34     else: return x    35     36 class Comparable:    37     38     "A date/datetime wrapper that allows comparisons with other types."    39     40     def __init__(self, dt):    41         self.dt = dt    42     43     def __cmp__(self, other):    44         dt = None    45         odt = None    46     47         # Find any dates/datetimes.    48     49         if isinstance(self.dt, date):    50             dt = self.dt    51         if isinstance(other, date):    52             odt = other    53         elif isinstance(other, Comparable):    54             if isinstance(other.dt, date):    55                 odt = other.dt    56             else:    57                 other = other.dt    58     59         if dt and odt:    60             return cmp(dt, odt)    61         elif dt:    62             return other.__rcmp__(dt)    63         elif odt:    64             return self.dt.__cmp__(odt)    65         else:    66             return self.dt.__cmp__(other)    67     68 class PointInTime:    69     70     "A base class for special values."    71     72     pass    73     74 class StartOfTime(PointInTime):    75     76     "A special value that compares earlier than other values."    77     78     def __cmp__(self, other):    79         if isinstance(other, StartOfTime):    80             return 0    81         else:    82             return -1    83     84     def __rcmp__(self, other):    85         return -self.__cmp__(other)    86     87     def __nonzero__(self):    88         return False    89     90     def __hash__(self):    91         return 0    92     93 class EndOfTime(PointInTime):    94     95     "A special value that compares later than other values."    96     97     def __cmp__(self, other):    98         if isinstance(other, EndOfTime):    99             return 0   100         else:   101             return 1   102    103     def __rcmp__(self, other):   104         return -self.__cmp__(other)   105    106     def __nonzero__(self):   107         return False   108    109     def __hash__(self):   110         return 0   111    112 class Endless:   113    114     "A special value indicating an endless period."   115    116     def __cmp__(self, other):   117         if isinstance(other, Endless):   118             return 0   119         else:   120             return 1   121    122     def __rcmp__(self, other):   123         return -self.__cmp__(other)   124    125     def __nonzero__(self):   126         return True   127    128 class PeriodBase:   129    130     "A basic period abstraction."   131    132     def __init__(self, start, end):   133         if isinstance(start, (date, PointInTime)):   134             self.start = start   135         else:   136             self.start = get_datetime(start) or StartOfTime()   137         if isinstance(end, (date, PointInTime)):   138             self.end = end   139         else:   140             self.end = get_datetime(end) or EndOfTime()   141    142     def as_tuple(self):   143         return self.start, self.end   144    145     def __hash__(self):   146         return hash((self.get_start(), self.get_end()))   147    148     def __cmp__(self, other):   149    150         "Return a comparison result against 'other' using points in time."   151    152         if isinstance(other, PeriodBase):   153             return cmp(   154                 (Comparable(ifnone(self.get_start_point(), StartOfTime())), Comparable(ifnone(self.get_end_point(), EndOfTime()))),   155                 (Comparable(ifnone(other.get_start_point(), StartOfTime())), Comparable(ifnone(other.get_end_point(), EndOfTime())))   156                 )   157         else:   158             return 1   159    160     def overlaps(self, other):   161         return Comparable(ifnone(self.get_end_point(), EndOfTime())) > Comparable(ifnone(other.get_start_point(), StartOfTime())) and \   162                Comparable(ifnone(self.get_start_point(), StartOfTime())) < Comparable(ifnone(other.get_end_point(), EndOfTime()))   163    164     def within(self, other):   165         return Comparable(ifnone(self.get_start_point(), StartOfTime())) >= Comparable(ifnone(other.get_start_point(), StartOfTime())) and \   166                Comparable(ifnone(self.get_end_point(), EndOfTime())) <= Comparable(ifnone(other.get_end_point(), EndOfTime()))   167    168     def common(self, other):   169         start = max(Comparable(ifnone(self.get_start_point(), StartOfTime())), Comparable(ifnone(other.get_start_point(), StartOfTime())))   170         end = min(Comparable(ifnone(self.get_end_point(), EndOfTime())), Comparable(ifnone(other.get_end_point(), EndOfTime())))   171         if start <= end:   172             return self.make_corrected(start.dt, end.dt)   173         else:   174             return None   175    176     def get_key(self):   177         return self.get_start(), self.get_end()   178    179     # Datetime and metadata methods.   180    181     def get_start(self):   182         return self.start   183    184     def get_end(self):   185         return self.end   186    187     def get_start_attr(self):   188         return get_datetime_attributes(self.start, self.tzid)   189    190     def get_end_attr(self):   191         return get_datetime_attributes(self.end, self.tzid)   192    193     def get_start_item(self):   194         return self.get_start(), self.get_start_attr()   195    196     def get_end_item(self):   197         return self.get_end(), self.get_end_attr()   198    199     def get_start_point(self):   200         return self.start   201    202     def get_end_point(self):   203         return self.end   204    205     def get_duration(self):   206         start = self.get_start_point()   207         end = self.get_end_point()   208         if start and end:   209             return end - start   210         else:   211             return Endless()   212    213 class Period(PeriodBase):   214    215     "A simple period abstraction."   216    217     def __init__(self, start, end, tzid=None, origin=None):   218    219         """   220         Initialise a period with the given 'start' and 'end', having a   221         contextual 'tzid', if specified, and an indicated 'origin'.   222    223         All metadata from the start and end points are derived from the supplied   224         dates/datetimes.   225         """   226    227         PeriodBase.__init__(self, start, end)   228         self.tzid = tzid   229         self.origin = origin   230    231     def as_tuple(self):   232         return self.start, self.end, self.tzid, self.origin   233    234     def __repr__(self):   235         return "Period%r" % (self.as_tuple(),)   236    237     # Datetime and metadata methods.   238    239     def get_tzid(self):   240         return get_tzid(self.get_start_attr(), self.get_end_attr()) or self.tzid   241    242     def get_start_point(self):   243         start = self.get_start()   244         if isinstance(start, PointInTime): return start   245         else: return to_utc_datetime(start, self.get_tzid())   246    247     def get_end_point(self):   248         end = self.get_end()   249         if isinstance(end, PointInTime): return end   250         else: return to_utc_datetime(end, self.get_tzid())   251    252     # Period and event recurrence logic.   253    254     def is_replaced(self, recurrenceids):   255    256         """   257         Return whether this period refers to one of the 'recurrenceids'.   258         The 'recurrenceids' should be normalised to UTC datetimes according to   259         time zone information provided by their objects or be floating dates or   260         datetimes requiring conversion using contextual time zone information.   261         """   262    263         for recurrenceid in recurrenceids:   264             if self.is_affected(recurrenceid):   265                 return recurrenceid   266         return None   267    268     def is_affected(self, recurrenceid):   269    270         """   271         Return whether this period refers to 'recurrenceid'. The 'recurrenceid'   272         should be normalised to UTC datetimes according to time zone information   273         provided by their objects. Otherwise, this period's contextual time zone   274         information is used to convert any date or floating datetime   275         representation to a point in time.   276         """   277    278         if not recurrenceid:   279             return None   280         d = get_recurrence_start(recurrenceid)   281         dt = get_recurrence_start_point(recurrenceid, self.tzid)   282         if self.get_start() == d or self.get_start_point() == dt:   283             return recurrenceid   284         return None   285    286     # Value correction methods.   287    288     def with_duration(self, duration):   289    290         """   291         Return a version of this period with the same start point but with the   292         given 'duration'.   293         """   294    295         return self.make_corrected(self.get_start(), self.get_start() + duration)   296    297     def check_permitted(self, permitted_values):   298    299         "Check the period against the given 'permitted_values'."   300    301         start = self.get_start()   302         end = self.get_end()   303         start_errors = check_permitted_values(start, permitted_values)   304         end_errors = check_permitted_values(end, permitted_values)   305    306         if not (start_errors or end_errors):   307             return None   308    309         return start_errors, end_errors   310    311     def get_corrected(self, permitted_values):   312    313         "Return a corrected version of this period."   314    315         errors = self.check_permitted(permitted_values)   316    317         if not errors:   318             return self   319    320         start_errors, end_errors = errors   321    322         start = self.get_start()   323         end = self.get_end()   324    325         if start_errors:   326             start = correct_datetime(start, permitted_values)   327         if end_errors:   328             end = correct_datetime(end, permitted_values)   329    330         return self.make_corrected(start, end)   331    332     def make_corrected(self, start, end):   333         return self.__class__(start, end, self.tzid, self.origin)   334    335 class FreeBusyPeriod(PeriodBase):   336    337     "A free/busy record abstraction."   338    339     def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None, expires=None):   340    341         """   342         Initialise a free/busy period with the given 'start' and 'end' points,   343         plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'   344         details.   345    346         An additional 'expires' parameter can be used to indicate an expiry   347         datetime in conjunction with free/busy offers made when countering   348         event proposals.   349         """   350    351         PeriodBase.__init__(self, start, end)   352         self.uid = uid   353         self.transp = transp   354         self.recurrenceid = recurrenceid   355         self.summary = summary   356         self.organiser = organiser   357         self.expires = expires   358    359     def as_tuple(self, strings_only=False):   360    361         """   362         Return the initialisation parameter tuple, converting false value   363         parameters to strings if 'strings_only' is set to a true value.   364         """   365    366         null = lambda x: (strings_only and [""] or [x])[0]   367         return (   368             strings_only and format_datetime(self.get_start_point()) or self.start,   369             strings_only and format_datetime(self.get_end_point()) or self.end,   370             self.uid or null(self.uid),   371             self.transp or strings_only and "OPAQUE" or None,   372             self.recurrenceid or null(self.recurrenceid),   373             self.summary or null(self.summary),   374             self.organiser or null(self.organiser),   375             self.expires or null(self.expires)   376             )   377    378     def __cmp__(self, other):   379    380         """   381         Compare this object to 'other', employing the uid if the periods   382         involved are the same.   383         """   384    385         result = PeriodBase.__cmp__(self, other)   386         if result == 0 and isinstance(other, FreeBusyPeriod):   387             return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))   388         else:   389             return result   390    391     def get_key(self):   392         return self.uid, self.recurrenceid, self.get_start()   393    394     def __repr__(self):   395         return "FreeBusyPeriod%r" % (self.as_tuple(),)   396    397     def get_tzid(self):   398         return "UTC"   399    400     # Period and event recurrence logic.   401    402     def is_replaced(self, recurrences):   403    404         """   405         Return whether this period refers to one of the 'recurrences'.   406         The 'recurrences' must be UTC datetimes corresponding to the start of   407         the period described by a recurrence.   408         """   409    410         for recurrence in recurrences:   411             if self.is_affected(recurrence):   412                 return True   413         return False   414    415     def is_affected(self, recurrence):   416    417         """   418         Return whether this period refers to 'recurrence'. The 'recurrence' must   419         be a UTC datetime corresponding to the start of the period described by   420         a recurrence.   421         """   422    423         return recurrence and self.get_start_point() == recurrence   424    425     # Value correction methods.   426    427     def make_corrected(self, start, end):   428         return self.__class__(start, end)   429    430 class RecurringPeriod(Period):   431        432     """   433     A period with iCalendar metadata attributes and origin information from an   434     object.   435     """   436        437     def __init__(self, start, end, tzid=None, origin=None, start_attr=None, end_attr=None):   438         Period.__init__(self, start, end, tzid, origin)   439         self.start_attr = start_attr   440         self.end_attr = end_attr   441    442     def get_start_attr(self):   443         return self.start_attr   444    445     def get_end_attr(self):   446         return self.end_attr   447    448     def as_tuple(self):   449         return self.start, self.end, self.tzid, self.origin, self.start_attr, self.end_attr   450        451     def __repr__(self):   452         return "RecurringPeriod%r" % (self.as_tuple(),)   453    454     def make_corrected(self, start, end):   455         return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())   456    457 # Time and period management.   458    459 def can_schedule(freebusy, periods, uid, recurrenceid):   460    461     """   462     Return whether the 'freebusy' list can accommodate the given 'periods'   463     employing the specified 'uid' and 'recurrenceid'.   464     """   465    466     for conflict in have_conflict(freebusy, periods, True):   467         if conflict.uid != uid or conflict.recurrenceid != recurrenceid:   468             return False   469    470     return True   471    472 def have_conflict(freebusy, periods, get_conflicts=False):   473    474     """   475     Return whether any period in 'freebusy' overlaps with the given 'periods',   476     returning a collection of such overlapping periods if 'get_conflicts' is   477     set to a true value.   478     """   479    480     conflicts = set()   481     for p in periods:   482         overlapping = period_overlaps(freebusy, p, get_conflicts)   483         if overlapping:   484             if get_conflicts:   485                 conflicts.update(overlapping)   486             else:   487                 return True   488    489     if get_conflicts:   490         return conflicts   491     else:   492         return False   493    494 def insert_period(freebusy, period):   495    496     "Insert into 'freebusy' the given 'period'."   497    498     i = bisect_left(freebusy, period)   499     if i == len(freebusy):   500         freebusy.append(period)   501     elif freebusy[i] != period:   502         freebusy.insert(i, period)   503    504 def remove_periods(freebusy, periods):   505    506     "Remove from 'freebusy' the given 'periods'."   507    508     for period in periods:   509         i = bisect_left(freebusy, period)   510         if i < len(freebusy) and freebusy[i] == period:   511             del freebusy[i]   512    513 def remove_event_periods(freebusy, uid, recurrenceid=None):   514    515     """   516     Remove from 'freebusy' all periods associated with 'uid' and 'recurrenceid'   517     (which if omitted causes the "parent" object's periods to be referenced).   518    519     Return the removed periods.   520     """   521    522     removed = []   523     i = 0   524     while i < len(freebusy):   525         fb = freebusy[i]   526         if fb.uid == uid and fb.recurrenceid == recurrenceid:   527             removed.append(freebusy[i])   528             del freebusy[i]   529         else:   530             i += 1   531    532     return removed   533    534 def remove_additional_periods(freebusy, uid, recurrenceids=None):   535    536     """   537     Remove from 'freebusy' all periods associated with 'uid' having a   538     recurrence identifier indicating an additional or modified period.   539    540     If 'recurrenceids' is specified, remove all periods associated with 'uid'   541     that do not have a recurrence identifier in the given list.   542     """   543    544     i = 0   545     while i < len(freebusy):   546         fb = freebusy[i]   547         if fb.uid == uid and fb.recurrenceid and (   548             recurrenceids is None or   549             recurrenceids is not None and fb.recurrenceid not in recurrenceids   550             ):   551             del freebusy[i]   552         else:   553             i += 1   554    555 def remove_affected_period(freebusy, uid, start):   556    557     """   558     Remove from 'freebusy' a period associated with 'uid' that provides an   559     occurrence starting at the given 'start' (provided by a recurrence   560     identifier, converted to a datetime). A recurrence identifier is used to   561     provide an alternative time period whilst also acting as a reference to the   562     originally-defined occurrence.   563     """   564    565     search = Period(start, start)   566     found = bisect_left(freebusy, search)   567    568     while found < len(freebusy):   569         fb = freebusy[found]   570    571         # Stop looking if the start no longer matches the recurrence identifier.   572    573         if fb.get_start_point() != search.get_start_point():   574             return   575    576         # If the period belongs to the parent object, remove it and return.   577    578         if not fb.recurrenceid and uid == fb.uid:   579             del freebusy[found]   580             break   581    582         # Otherwise, keep looking for a matching period.   583    584         found += 1   585    586 def periods_from(freebusy, period):   587    588     "Return the entries in 'freebusy' at or after 'period'."   589    590     first = bisect_left(freebusy, period)   591     return freebusy[first:]   592    593 def periods_until(freebusy, period):   594    595     "Return the entries in 'freebusy' before 'period'."   596    597     last = bisect_right(freebusy, Period(period.get_end(), period.get_end(), period.get_tzid()))   598     return freebusy[:last]   599    600 def get_overlapping(freebusy, period):   601    602     """   603     Return the entries in 'freebusy' providing periods overlapping with   604     'period'.   605     """   606    607     # Find the range of periods potentially overlapping the period in the   608     # free/busy collection.   609    610     startpoints = periods_until(freebusy, period)   611    612     # Find the range of periods potentially overlapping the period in a version   613     # of the free/busy collection sorted according to end datetimes.   614    615     endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]   616     endpoints.sort()   617     first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))   618     endpoints = endpoints[first:]   619    620     overlapping = set()   621    622     for p, fb in endpoints:   623         if fb.overlaps(period):   624             overlapping.add(fb)   625    626     overlapping = list(overlapping)   627     overlapping.sort()   628     return overlapping   629    630 def period_overlaps(freebusy, period, get_periods=False):   631    632     """   633     Return whether any period in 'freebusy' overlaps with the given 'period',   634     returning a collection of overlapping periods if 'get_periods' is set to a   635     true value.   636     """   637    638     overlapping = get_overlapping(freebusy, period)   639    640     if get_periods:   641         return overlapping   642     else:   643         return len(overlapping) != 0   644    645 def remove_overlapping(freebusy, period):   646    647     "Remove from 'freebusy' all periods overlapping with 'period'."   648    649     overlapping = get_overlapping(freebusy, period)   650    651     if overlapping:   652         for fb in overlapping:   653             freebusy.remove(fb)   654    655 def replace_overlapping(freebusy, period, replacements):   656    657     """   658     Replace existing periods in 'freebusy' within the given 'period', using the   659     given 'replacements'.   660     """   661    662     remove_overlapping(freebusy, period)   663     for replacement in replacements:   664         insert_period(freebusy, replacement)   665    666 def coalesce_freebusy(freebusy):   667    668     "Coalesce the periods in 'freebusy'."   669    670     if not freebusy:   671         return freebusy   672    673     fb = []   674     start = freebusy[0].get_start_point()   675     end = freebusy[0].get_end_point()   676    677     for period in freebusy[1:]:   678         if period.get_start_point() > end:   679             fb.append(FreeBusyPeriod(start, end))   680             start = period.get_start_point()   681             end = period.get_end_point()   682         else:   683             end = max(end, period.get_end_point())   684    685     fb.append(FreeBusyPeriod(start, end))   686     return fb   687    688 def invert_freebusy(freebusy):   689    690     "Return the free periods from 'freebusy'."   691    692     if not freebusy:   693         return [FreeBusyPeriod(None, None)]   694    695     # Coalesce periods that overlap or are adjacent.   696    697     fb = coalesce_freebusy(freebusy)   698     free = []   699    700     # Add a start-of-time period if appropriate.   701    702     first = fb[0].get_start_point()   703     if first:   704         free.append(FreeBusyPeriod(None, first))   705    706     start = fb[0].get_end_point()   707    708     for period in fb[1:]:   709         free.append(FreeBusyPeriod(start, period.get_start_point()))   710         start = period.get_end_point()   711    712     # Add an end-of-time period if appropriate.   713    714     if start:   715         free.append(FreeBusyPeriod(start, None))   716    717     return free   718    719 # Period layout.   720    721 def get_scale(periods, tzid, view_period=None):   722    723     """   724     Return a time scale from the given list of 'periods'.   725    726     The given 'tzid' is used to make sure that the times are defined according   727     to the chosen time zone.   728    729     An optional 'view_period' is used to constrain the scale to the given   730     period.   731    732     The returned scale is a mapping from time to (starting, ending) tuples,   733     where starting and ending are collections of periods.   734     """   735    736     scale = {}   737     view_start = view_period and to_timezone(view_period.get_start_point(), tzid) or None   738     view_end = view_period and to_timezone(view_period.get_end_point(), tzid) or None   739    740     for p in periods:   741    742         # Add a point and this event to the starting list.   743    744         start = to_timezone(p.get_start(), tzid)   745         start = view_start and max(start, view_start) or start   746         if not scale.has_key(start):   747             scale[start] = [], []   748         scale[start][0].append(p)   749    750         # Add a point and this event to the ending list.   751    752         end = to_timezone(p.get_end(), tzid)   753         end = view_end and min(end, view_end) or end   754         if not scale.has_key(end):   755             scale[end] = [], []   756         scale[end][1].append(p)   757    758     return scale   759    760 class Point:   761    762     "A qualified point in time."   763    764     PRINCIPAL, REPEATED = 0, 1   765    766     def __init__(self, point, indicator=None):   767         self.point = point   768         self.indicator = indicator or self.PRINCIPAL   769    770     def __hash__(self):   771         return hash((self.point, self.indicator))   772    773     def __cmp__(self, other):   774         if isinstance(other, Point):   775             return cmp((self.point, self.indicator), (other.point, other.indicator))   776         elif isinstance(other, datetime):   777             return cmp(self.point, other)   778         else:   779             return 1   780    781     def __eq__(self, other):   782         return self.__cmp__(other) == 0   783    784     def __ne__(self, other):   785         return not self == other   786    787     def __lt__(self, other):   788         return self.__cmp__(other) < 0   789    790     def __le__(self, other):   791         return self.__cmp__(other) <= 0   792    793     def __gt__(self, other):   794         return not self <= other   795    796     def __ge__(self, other):   797         return not self < other   798    799     def __repr__(self):   800         return "Point(%r, Point.%s)" % (self.point, self.indicator and "REPEATED" or "PRINCIPAL")   801    802 def get_slots(scale):   803    804     """   805     Return an ordered list of time slots from the given 'scale'.   806    807     Each slot is a tuple containing details of a point in time for the start of   808     the slot, together with a list of parallel event periods.   809    810     Each point in time is described as a Point representing the actual point in   811     time together with an indicator of the nature of the point in time (as a   812     principal point in a time scale or as a repeated point used to terminate   813     events occurring for an instant in time).   814     """   815    816     slots = []   817     active = []   818    819     points = scale.items()   820     points.sort()   821    822     for point, (starting, ending) in points:   823         ending = set(ending)   824         instants = ending.intersection(starting)   825    826         # Discard all active events ending at or before this start time.   827         # Free up the position in the active list.   828    829         for t in ending.difference(instants):   830             i = active.index(t)   831             active[i] = None   832    833         # For each event starting at the current point, fill any newly-vacated   834         # position or add to the end of the active list.   835    836         for t in starting:   837             try:   838                 i = active.index(None)   839                 active[i] = t   840             except ValueError:   841                 active.append(t)   842    843         # Discard vacant positions from the end of the active list.   844    845         while active and active[-1] is None:   846             active.pop()   847    848         # Add an entry for the time point before "instants".   849    850         slots.append((Point(point), active[:]))   851    852         # Discard events ending at the same time as they began.   853    854         if instants:   855             for t in instants:   856                 i = active.index(t)   857                 active[i] = None   858    859             # Discard vacant positions from the end of the active list.   860    861             while active and active[-1] is None:   862                 active.pop()   863    864             # Add another entry for the time point after "instants".   865    866             slots.append((Point(point, Point.REPEATED), active[:]))   867    868     return slots   869    870 def add_day_start_points(slots, tzid):   871    872     """   873     Introduce into the 'slots' any day start points required by multi-day   874     periods. The 'tzid' is required to make sure that appropriate time zones   875     are chosen and not necessarily those provided by the existing time points.   876     """   877    878     new_slots = []   879     current_date = None   880     previously_active = []   881    882     for point, active in slots:   883         start_of_day = get_start_of_day(point.point, tzid)   884         this_date = point.point.date()   885    886         # For each new day, add a slot for the start of the day where periods   887         # are active and where no such slot already exists.   888    889         if this_date != current_date:   890    891             # Fill in days where events remain active.   892    893             if current_date:   894                 current_date += timedelta(1)   895                 while current_date < this_date:   896                     new_slots.append((Point(get_start_of_day(current_date, tzid)), previously_active))   897                     current_date += timedelta(1)   898             else:   899                 current_date = this_date   900    901             # Add any continuing periods.   902    903             if point.point != start_of_day:   904                 new_slots.append((Point(start_of_day), previously_active))   905    906         # Add the currently active periods at this point in time.   907    908         previously_active = active   909    910     for t in new_slots:   911         insort_left(slots, t)   912    913 def remove_end_slot(slots, view_period):   914    915     """   916     Remove from 'slots' any slot situated at the end of the given 'view_period'.   917     """   918    919     end = view_period.get_end_point()   920     if not end or not slots:   921         return   922     i = bisect_left(slots, (Point(end), None))   923     if i < len(slots):   924         del slots[i:]   925    926 def add_slots(slots, points):   927    928     """   929     Introduce into the 'slots' entries for those in 'points' that are not   930     already present, propagating active periods from time points preceding   931     those added.   932     """   933    934     new_slots = []   935    936     for point in points:   937         i = bisect_left(slots, (point,)) # slots is [(point, active)...]   938         if i < len(slots) and slots[i][0] == point:   939             continue   940    941         new_slots.append((point, i > 0 and slots[i-1][1] or []))   942    943     for t in new_slots:   944         insort_left(slots, t)   945    946 def partition_by_day(slots):   947    948     """   949     Return a mapping from dates to time points provided by 'slots'.   950     """   951    952     d = {}   953    954     for point, value in slots:   955         day = point.point.date()   956         if not d.has_key(day):   957             d[day] = []   958         d[day].append((point, value))   959    960     return d   961    962 def add_empty_days(days, tzid, start=None, end=None):   963    964     """   965     Add empty days to 'days' between busy days, and optionally from the given   966     'start' day and until the given 'end' day.   967     """   968    969     last_day = start - timedelta(1)   970     all_days = days.keys()   971     all_days.sort()   972    973     for day in all_days:   974         if last_day:   975             empty_day = last_day + timedelta(1)   976             while empty_day < day:   977                 days[empty_day] = [(Point(get_start_of_day(empty_day, tzid)), None)]   978                 empty_day += timedelta(1)   979         last_day = day   980    981     if end:   982         empty_day = last_day + timedelta(1)   983         while empty_day < end:   984             days[empty_day] = [(Point(get_start_of_day(empty_day, tzid)), None)]   985             empty_day += timedelta(1)   986    987 def get_spans(slots):   988    989     "Inspect the given 'slots', returning a mapping of period keys to spans."   990    991     points = [point for point, active in slots]   992     spans = {}   993    994     for _point, active in slots:   995         for p in active:   996             if p:   997                 key = p.get_key()   998                 start_slot = bisect_left(points, p.get_start())   999                 end_slot = bisect_left(points, p.get_end())  1000                 spans[key] = end_slot - start_slot  1001   1002     return spans  1003   1004 def update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):  1005   1006     """  1007     Update the free/busy details with the given 'periods', 'transp' setting,  1008     'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.  1009   1010     An optional 'expires' datetime string indicates the expiry time of any  1011     free/busy offer.  1012     """  1013   1014     remove_event_periods(freebusy, uid, recurrenceid)  1015   1016     for p in periods:  1017         insert_period(freebusy, FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))  1018   1019 # vim: tabstop=4 expandtab shiftwidth=4