imip-agent

Annotated imiptools/period.py

199:1d7cd23a754e
2015-01-30 Paul Boddie Incorporated end points in slot details so that such information can be used in form controls for new event creation.
paul@48 1
#!/usr/bin/env python
paul@48 2
paul@146 3
"""
paul@146 4
Managing and presenting periods of time.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@146 20
"""
paul@146 21
paul@48 22
from bisect import bisect_left, insort_left
paul@153 23
from imiptools.dates import get_datetime, get_start_of_day, to_timezone
paul@48 24
paul@48 25
# Time management.
paul@48 26
paul@72 27
def have_conflict(freebusy, periods, get_conflicts=False):
paul@72 28
paul@72 29
    """
paul@72 30
    Return whether any period in 'freebusy' overlaps with the given 'periods',
paul@72 31
    returning a collection of such overlapping periods if 'get_conflicts' is
paul@72 32
    set to a true value.
paul@72 33
    """
paul@72 34
paul@72 35
    conflicts = []
paul@72 36
    for start, end in periods:
paul@74 37
        overlapping = period_overlaps(freebusy, (start, end), get_conflicts)
paul@74 38
        if overlapping:
paul@72 39
            if get_conflicts:
paul@74 40
                conflicts += overlapping
paul@72 41
            else:
paul@72 42
                return True
paul@74 43
paul@72 44
    if get_conflicts:
paul@72 45
        return conflicts
paul@72 46
    else:
paul@72 47
        return False
paul@72 48
paul@48 49
def insert_period(freebusy, period):
paul@48 50
    insort_left(freebusy, period)
paul@48 51
paul@48 52
def remove_period(freebusy, uid):
paul@48 53
    i = 0
paul@48 54
    while i < len(freebusy):
paul@48 55
        t = freebusy[i]
paul@48 56
        if len(t) >= 3 and t[2] == uid:
paul@48 57
            del freebusy[i]
paul@48 58
        else:
paul@48 59
            i += 1
paul@48 60
paul@74 61
def period_overlaps(freebusy, period, get_periods=False):
paul@72 62
paul@72 63
    """
paul@74 64
    Return whether any period in 'freebusy' overlaps with the given 'period',
paul@74 65
    returning a collection of overlapping periods if 'get_periods' is set to a
paul@74 66
    true value.
paul@72 67
    """
paul@72 68
paul@48 69
    dtstart, dtend = period[:2]
paul@112 70
    found = bisect_left(freebusy, (dtstart, dtend, None, None))
paul@74 71
paul@74 72
    overlapping = []
paul@74 73
paul@74 74
    # Find earlier overlapping periods.
paul@74 75
paul@74 76
    i = found
paul@74 77
paul@74 78
    while i > 0 and freebusy[i - 1][1] > dtstart:
paul@74 79
        if get_periods:
paul@74 80
            overlapping.insert(0, freebusy[i - 1])
paul@74 81
        else:
paul@74 82
            return True
paul@74 83
        i -= 1
paul@74 84
paul@74 85
    # Find later overlapping periods.
paul@74 86
paul@74 87
    i = found
paul@74 88
paul@74 89
    while i < len(freebusy) and (dtend is None or freebusy[i][0] < dtend):
paul@74 90
        if get_periods:
paul@74 91
            overlapping.append(freebusy[i])
paul@74 92
        else:
paul@74 93
            return True
paul@74 94
        i += 1
paul@74 95
paul@74 96
    if get_periods:
paul@74 97
        return overlapping
paul@74 98
    else:
paul@74 99
        return False
paul@48 100
paul@113 101
# Period layout.
paul@113 102
paul@162 103
def convert_periods(periods, tzid):
paul@162 104
paul@162 105
    "Convert 'periods' to use datetime objects employing the given 'tzid'."
paul@162 106
paul@162 107
    l = []
paul@162 108
paul@162 109
    for t in periods:
paul@162 110
        start, end = t[:2]
paul@162 111
        start = to_timezone(get_datetime(start), tzid)
paul@162 112
        end = to_timezone(get_datetime(end), tzid)
paul@162 113
        l.append((start, end) + tuple(t[2:]))
paul@162 114
paul@162 115
    return l
paul@162 116
paul@162 117
def get_scale(periods):
paul@113 118
paul@113 119
    """
paul@162 120
    Return an ordered time scale from the given list 'periods', with the first
paul@162 121
    two elements of each tuple being start and end times.
paul@153 122
paul@162 123
    The given 'tzid' is used to make sure that the times are defined according
paul@162 124
    to the chosen time zone.
paul@162 125
paul@162 126
    The returned scale is a mapping from time to (starting, ending) tuples,
paul@162 127
    where starting and ending are collections of tuples from 'periods'.
paul@113 128
    """
paul@113 129
paul@113 130
    scale = {}
paul@113 131
paul@162 132
    for t in periods:
paul@113 133
        start, end = t[:2]
paul@113 134
paul@113 135
        # Add a point and this event to the starting list.
paul@113 136
paul@113 137
        if not scale.has_key(start):
paul@113 138
            scale[start] = [], []
paul@113 139
        scale[start][0].append(t)
paul@113 140
paul@113 141
        # Add a point and this event to the ending list.
paul@113 142
paul@113 143
        if not scale.has_key(end):
paul@113 144
            scale[end] = [], []
paul@113 145
        scale[end][1].append(t)
paul@113 146
paul@113 147
    return scale
paul@113 148
paul@162 149
def get_slots(scale):
paul@113 150
paul@113 151
    """
paul@162 152
    Return an ordered list of time slots from the given 'scale'.
paul@113 153
paul@199 154
    Each slot is a tuple of the form (point, (endpoint, active)), where 'point'
paul@199 155
    indicates the start of the slot, 'endpoint' the end of the slot (or None),
paul@199 156
    and 'active' provides a list of parallel event tuples, with each tuple
paul@199 157
    containing the original details of an event.
paul@113 158
    """
paul@113 159
paul@113 160
    slots = []
paul@113 161
    active = []
paul@113 162
paul@162 163
    points = scale.items()
paul@162 164
    points.sort()
paul@162 165
paul@199 166
    # Maintain a current slot so that the end can be added.
paul@199 167
paul@199 168
    current = None
paul@199 169
paul@162 170
    for point, (starting, ending) in points:
paul@113 171
paul@113 172
        # Discard all active events ending at or before this start time.
paul@161 173
        # Free up the position in the active list.
paul@113 174
paul@113 175
        for t in ending:
paul@113 176
            i = active.index(t)
paul@113 177
            active[i] = None
paul@113 178
paul@161 179
        # For each event starting at the current point, fill any newly-vacated
paul@161 180
        # position or add to the end of the active list.
paul@161 181
paul@113 182
        for t in starting:
paul@113 183
            try:
paul@113 184
                i = active.index(None)
paul@113 185
                active[i] = t
paul@113 186
            except ValueError:
paul@113 187
                active.append(t)
paul@113 188
paul@161 189
        # Discard vacant positions from the end of the active list.
paul@161 190
paul@113 191
        while active and active[-1] is None:
paul@113 192
            active.pop()
paul@113 193
paul@199 194
        if current:
paul@199 195
            _point, _active = current
paul@199 196
            slots.append((_point, (point, _active)))
paul@199 197
        current = point, active[:]
paul@199 198
paul@199 199
    if current:
paul@199 200
        _point, _active = current
paul@199 201
        slots.append((_point, (None, _active)))
paul@113 202
paul@113 203
    return slots
paul@113 204
paul@162 205
def add_day_start_points(slots):
paul@153 206
paul@153 207
    """
paul@162 208
    Introduce into the 'slots' any day start points required by multi-day
paul@162 209
    periods.
paul@153 210
    """
paul@153 211
paul@162 212
    new_slots = []
paul@153 213
    current_date = None
paul@199 214
    previously_active = []
paul@153 215
paul@199 216
    for point, (endpoint, active) in slots:
paul@162 217
        start_of_day = get_start_of_day(point)
paul@162 218
        this_date = point.date()
paul@153 219
paul@199 220
        # For each new day, add a slot for the start of the day where no such
paul@199 221
        # slot already exists.
paul@153 222
paul@153 223
        if this_date != current_date:
paul@153 224
            current_date = this_date
paul@153 225
paul@153 226
            # Add any continuing periods.
paul@153 227
paul@199 228
            if point != start_of_day:
paul@199 229
                new_slots.append((start_of_day, (point, previously_active)))
paul@153 230
paul@153 231
        # Add the currently active periods at this point in time.
paul@153 232
paul@153 233
        previously_active = active
paul@153 234
paul@162 235
    for t in new_slots:
paul@162 236
        insort_left(slots, t)
paul@162 237
paul@162 238
def add_slots(slots, points):
paul@162 239
paul@162 240
    """
paul@162 241
    Introduce into the 'slots' entries for those in 'points' that are not
paul@170 242
    already present, propagating active periods from time points preceding
paul@170 243
    those added.
paul@162 244
    """
paul@162 245
paul@199 246
    for point in points:
paul@199 247
        i = bisect_left(slots, (point, ()))
paul@162 248
paul@199 249
        # Ignore points of time for which slots already exist.
paul@199 250
paul@162 251
        if i < len(slots) and slots[i][0] == point:
paul@162 252
            continue
paul@162 253
paul@199 254
        # Update the endpoint of any preceding slot to refer to this new point.
paul@199 255
paul@199 256
        if i > 0:
paul@199 257
            _point, (_endpoint, previously_active) = slots[i-1]
paul@199 258
            slots[i-1] = _point, (point, previously_active)
paul@199 259
        else:
paul@199 260
            previously_active = []
paul@162 261
paul@199 262
        # Either insert a new slot before any following ones.
paul@199 263
paul@199 264
        if i < len(slots):
paul@199 265
            _point, (_endpoint, _active) = slots[i]
paul@199 266
            slots.insert(i, (point, (_point, previously_active)))
paul@199 267
paul@199 268
        # Or add a new slot at the end of the list.
paul@199 269
paul@199 270
        else:
paul@199 271
            slots.append((point, (None, previously_active)))
paul@162 272
paul@162 273
def partition_by_day(slots):
paul@162 274
paul@162 275
    """
paul@199 276
    Return a mapping from dates to entries provided by 'slots'.
paul@162 277
    """
paul@162 278
paul@162 279
    d = {}
paul@162 280
paul@199 281
    for point, (endpoint, value) in slots:
paul@162 282
        day = point.date()
paul@162 283
        if not d.has_key(day):
paul@162 284
            d[day] = []
paul@199 285
        d[day].append((point, (endpoint, value)))
paul@162 286
paul@162 287
    return d
paul@153 288
paul@114 289
def get_spans(slots):
paul@114 290
paul@114 291
    "Inspect the given 'slots', returning a mapping of event uids to spans."
paul@114 292
paul@199 293
    points = [point for point, (endpoint, active) in slots]
paul@114 294
    spans = {}
paul@114 295
paul@199 296
    for point, (endpoint, active) in slots:
paul@114 297
        for t in active:
paul@185 298
            if t and len(t) >= 2:
paul@185 299
                start, end, uid, key = get_freebusy_details(t)
paul@185 300
paul@153 301
                try:
paul@153 302
                    start_slot = points.index(start)
paul@153 303
                except ValueError:
paul@153 304
                    start_slot = 0
paul@153 305
                try:
paul@153 306
                    end_slot = points.index(end)
paul@153 307
                except ValueError:
paul@153 308
                    end_slot = len(slots)
paul@185 309
                spans[key] = end_slot - start_slot
paul@114 310
paul@114 311
    return spans
paul@114 312
paul@185 313
def get_freebusy_details(t):
paul@185 314
paul@185 315
    "Return a tuple of the form (start, end, uid, key) from 't'."
paul@185 316
paul@185 317
    # Handle both complete free/busy details...
paul@185 318
paul@185 319
    if len(t) > 2:
paul@185 320
        start, end, uid = t[:3]
paul@185 321
        key = uid
paul@185 322
paul@185 323
    # ...and published details without specific event details.
paul@185 324
paul@185 325
    else:
paul@185 326
        start, end = t[:2]
paul@185 327
        uid = None
paul@185 328
        key = (start, end)
paul@185 329
paul@185 330
    return start, end, uid, key
paul@185 331
paul@48 332
# vim: tabstop=4 expandtab shiftwidth=4