imip-agent

imiptools/period.py

162:ba7f19be84c0
2015-01-23 Paul Boddie Added initial support for presenting incoming requests alongside the free/busy information already available. This requires each group of periods to remain separated whilst exposing the same scale of time points in all of them.
     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, insort_left    23 from imiptools.dates import get_datetime, get_start_of_day, to_timezone    24     25 # Time management.    26     27 def have_conflict(freebusy, periods, get_conflicts=False):    28     29     """    30     Return whether any period in 'freebusy' overlaps with the given 'periods',    31     returning a collection of such overlapping periods if 'get_conflicts' is    32     set to a true value.    33     """    34     35     conflicts = []    36     for start, end in periods:    37         overlapping = period_overlaps(freebusy, (start, end), get_conflicts)    38         if overlapping:    39             if get_conflicts:    40                 conflicts += overlapping    41             else:    42                 return True    43     44     if get_conflicts:    45         return conflicts    46     else:    47         return False    48     49 def insert_period(freebusy, period):    50     insort_left(freebusy, period)    51     52 def remove_period(freebusy, uid):    53     i = 0    54     while i < len(freebusy):    55         t = freebusy[i]    56         if len(t) >= 3 and t[2] == uid:    57             del freebusy[i]    58         else:    59             i += 1    60     61 def period_overlaps(freebusy, period, get_periods=False):    62     63     """    64     Return whether any period in 'freebusy' overlaps with the given 'period',    65     returning a collection of overlapping periods if 'get_periods' is set to a    66     true value.    67     """    68     69     dtstart, dtend = period[:2]    70     found = bisect_left(freebusy, (dtstart, dtend, None, None))    71     72     overlapping = []    73     74     # Find earlier overlapping periods.    75     76     i = found    77     78     while i > 0 and freebusy[i - 1][1] > dtstart:    79         if get_periods:    80             overlapping.insert(0, freebusy[i - 1])    81         else:    82             return True    83         i -= 1    84     85     # Find later overlapping periods.    86     87     i = found    88     89     while i < len(freebusy) and (dtend is None or freebusy[i][0] < dtend):    90         if get_periods:    91             overlapping.append(freebusy[i])    92         else:    93             return True    94         i += 1    95     96     if get_periods:    97         return overlapping    98     else:    99         return False   100    101 # Period layout.   102    103 def convert_periods(periods, tzid):   104    105     "Convert 'periods' to use datetime objects employing the given 'tzid'."   106    107     l = []   108    109     for t in periods:   110         start, end = t[:2]   111         start = to_timezone(get_datetime(start), tzid)   112         end = to_timezone(get_datetime(end), tzid)   113         l.append((start, end) + tuple(t[2:]))   114    115     return l   116    117 def get_scale(periods):   118    119     """   120     Return an ordered time scale from the given list 'periods', with the first   121     two elements of each tuple being start and end times.   122    123     The given 'tzid' is used to make sure that the times are defined according   124     to the chosen time zone.   125    126     The returned scale is a mapping from time to (starting, ending) tuples,   127     where starting and ending are collections of tuples from 'periods'.   128     """   129    130     scale = {}   131    132     for t in periods:   133         start, end = t[:2]   134    135         # Add a point and this event to the starting list.   136    137         if not scale.has_key(start):   138             scale[start] = [], []   139         scale[start][0].append(t)   140    141         # Add a point and this event to the ending list.   142    143         if not scale.has_key(end):   144             scale[end] = [], []   145         scale[end][1].append(t)   146    147     return scale   148    149 def get_slots(scale):   150    151     """   152     Return an ordered list of time slots from the given 'scale'.   153    154     Each slot is a tuple containing a point in time for the start of the slot,   155     together with a list of parallel event tuples, each tuple containing the   156     original details of an event.   157     """   158    159     slots = []   160     active = []   161    162     points = scale.items()   163     points.sort()   164    165     for point, (starting, ending) in points:   166    167         # Discard all active events ending at or before this start time.   168         # Free up the position in the active list.   169    170         for t in ending:   171             i = active.index(t)   172             active[i] = None   173    174         # For each event starting at the current point, fill any newly-vacated   175         # position or add to the end of the active list.   176    177         for t in starting:   178             try:   179                 i = active.index(None)   180                 active[i] = t   181             except ValueError:   182                 active.append(t)   183    184         # Discard vacant positions from the end of the active list.   185    186         while active and active[-1] is None:   187             active.pop()   188    189         slots.append((point, active[:]))   190    191     return slots   192    193 def add_day_start_points(slots):   194    195     """   196     Introduce into the 'slots' any day start points required by multi-day   197     periods.   198     """   199    200     new_slots = []   201     current_date = None   202     previously_active = None   203    204     for point, active in slots:   205         start_of_day = get_start_of_day(point)   206         this_date = point.date()   207    208         # For each new day, create a partition of the original collection.   209    210         if this_date != current_date:   211             current_date = this_date   212    213             # Add any continuing periods.   214    215             if point != start_of_day and previously_active:   216                 new_slots.append((start_of_day, previously_active))   217    218         # Add the currently active periods at this point in time.   219    220         previously_active = active   221    222     for t in new_slots:   223         insort_left(slots, t)   224    225 def add_slots(slots, points):   226    227     """   228     Introduce into the 'slots' entries for those in 'points' that are not   229     already present, propagating active periods from time points preceding and   230     succeeding those added.   231     """   232    233     new_slots = []   234    235     for point in points:   236         i = bisect_left(slots, (point, None))   237         if i < len(slots) and slots[i][0] == point:   238             continue   239    240         previously_active = i > 0 and slots[i-1] or []   241         subsequently_active = i < len(slots) and slots[i] or []   242    243         active = []   244    245         for p, s in zip(previously_active, subsequently_active):   246             if p == s:   247                 active.append(p)   248             else:   249                 active.append(None)   250    251         new_slots.append((point, active))   252    253     for t in new_slots:   254         insort_left(slots, t)   255    256 def partition_by_day(slots):   257    258     """   259     Return a mapping from dates to time points provided by 'slots'.   260     """   261    262     d = {}   263    264     for point, value in slots:   265         day = point.date()   266         if not d.has_key(day):   267             d[day] = []   268         d[day].append((point, value))   269    270     return d   271    272 def get_spans(slots):   273    274     "Inspect the given 'slots', returning a mapping of event uids to spans."   275    276     points = [point for point, active in slots]   277     spans = {}   278    279     for point, active in slots:   280         for t in active:   281             if t:   282                 start, end, uid = t[:3]   283                 try:   284                     start_slot = points.index(start)   285                 except ValueError:   286                     start_slot = 0   287                 try:   288                     end_slot = points.index(end)   289                 except ValueError:   290                     end_slot = len(slots)   291                 spans[uid] = end_slot - start_slot   292    293     return spans   294    295 # vim: tabstop=4 expandtab shiftwidth=4