imip-agent

imiptools/period.py

198:9e3d70023691
2015-01-30 Paul Boddie Fixed comment.
     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, add a slot for the start of the day where periods   209         # are active and where no such slot already exists.   210    211         if this_date != current_date:   212             current_date = this_date   213    214             # Add any continuing periods.   215    216             if point != start_of_day and previously_active:   217                 new_slots.append((start_of_day, previously_active))   218    219         # Add the currently active periods at this point in time.   220    221         previously_active = active   222    223     for t in new_slots:   224         insort_left(slots, t)   225    226 def add_slots(slots, points):   227    228     """   229     Introduce into the 'slots' entries for those in 'points' that are not   230     already present, propagating active periods from time points preceding   231     those added.   232     """   233    234     new_slots = []   235    236     for point in points:   237         i = bisect_left(slots, (point, None))   238         if i < len(slots) and slots[i][0] == point:   239             continue   240    241         new_slots.append((point, i > 0 and slots[i-1][1] or []))   242    243     for t in new_slots:   244         insort_left(slots, t)   245    246 def partition_by_day(slots):   247    248     """   249     Return a mapping from dates to time points provided by 'slots'.   250     """   251    252     d = {}   253    254     for point, value in slots:   255         day = point.date()   256         if not d.has_key(day):   257             d[day] = []   258         d[day].append((point, value))   259    260     return d   261    262 def get_spans(slots):   263    264     "Inspect the given 'slots', returning a mapping of event uids to spans."   265    266     points = [point for point, active in slots]   267     spans = {}   268    269     for point, active in slots:   270         for t in active:   271             if t and len(t) >= 2:   272                 start, end, uid, key = get_freebusy_details(t)   273    274                 try:   275                     start_slot = points.index(start)   276                 except ValueError:   277                     start_slot = 0   278                 try:   279                     end_slot = points.index(end)   280                 except ValueError:   281                     end_slot = len(slots)   282                 spans[key] = end_slot - start_slot   283    284     return spans   285    286 def get_freebusy_details(t):   287    288     "Return a tuple of the form (start, end, uid, key) from 't'."   289    290     # Handle both complete free/busy details...   291    292     if len(t) > 2:   293         start, end, uid = t[:3]   294         key = uid   295    296     # ...and published details without specific event details.   297    298     else:   299         start, end = t[:2]   300         uid = None   301         key = (start, end)   302    303     return start, end, uid, key   304    305 # vim: tabstop=4 expandtab shiftwidth=4