Source code for hyperstream.time_interval

# The MIT License (MIT) # Copyright (c) 2014-2017 University of Bristol
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
"""
Module for dealing with time intervals containing TimeInterval, TimeIntervals, and RelativeTimeInterval
"""

from utils import MIN_DATE, utcnow, UTC, Printable, get_timedelta

from datetime import date, datetime, timedelta
from dateutil.parser import parse


[docs]class TimeIntervals(Printable): """ Container class for time intervals, that manages splitting and joining Example object: (t1,t2] U (t3,t4] U ... """ def __init__(self, intervals=None): """ Initialise the object with the given intervals. These should be in a format that can be parsed by parse_time_tuple :param intervals: The time intervals """ self.intervals = [] if intervals: for v in intervals: if isinstance(v, (tuple, list)): if len(v) != 2: raise TypeError() v = parse_time_tuple(*v) elif not isinstance(v, TimeInterval): raise TypeError("Expected tuple/list/TimeInterval ({} given)".format(type(v))) self.intervals.append(v) def __str__(self): return " U ".join(map(str, self.intervals)) if self.intervals else "[]" def __repr__(self): return "{}([{}])".format(self.__class__.__name__, ", ".join(map(repr, self.intervals))) @property def is_empty(self): return len(self.intervals) == 0 @property def start(self): return min(self.intervals, key=lambda x: x.start).start @property def end(self): return max(self.intervals, key=lambda x: x.end).end @property def span(self): return TimeInterval(self.start, self.end)
[docs] def split(self, points): if len(points) == 0: return p = points[-1] for i in range(len(self.intervals)): if (self.intervals[i].start < p) and (self.intervals[i].end > p): self.intervals = self.intervals[:i] \ + [TimeInterval(self.intervals[i].start, p), TimeInterval(p, self.intervals[i].end)] \ + self.intervals[(i + 1):] self.split(points[:-1])
[docs] def compress(self): if len(self.intervals) == 0: return v = self.intervals[:1] for i in range(1, len(self.intervals)): if self.intervals[i].start == v[-1].end: v[-1] = TimeInterval(v[-1].start, self.intervals[i].end) else: v.append(self.intervals[i]) self.intervals = v
def __add__(self, other): self_points = [point for interval in self.intervals for point in (interval.start, interval.end)] other_points = [point for interval in other.intervals for point in (interval.start, interval.end)] self.split(other_points) other.split(self_points) v = list(set(self.intervals).union(set(other.intervals))) v.sort(key=lambda ii: ii.start) new = TimeIntervals(v) self.compress() other.compress() new.compress() return new def __sub__(self, other): self_points = [point for interval in self.intervals for point in (interval.start, interval.end)] other_points = [point for interval in other.intervals for point in (interval.start, interval.end)] self.split(other_points) other.split(self_points) v = list(set(self.intervals).difference(set(other.intervals))) v.sort(key=lambda ii: ii.start) new = TimeIntervals(v) self.compress() other.compress() new.compress() return new def __eq__(self, other): return isinstance(other, TimeIntervals) and all(z[0] == z[1] for z in zip(self.intervals, other.intervals)) def __ne__(self, other): return not self == other def __iter__(self): return iter(sorted(self.intervals)) def __getitem__(self, key): if isinstance(key, slice): items = self.intervals[key] if isinstance(items, TimeInterval): return items return TimeIntervals(items) return self.intervals[key] def __bool__(self): return self.intervals is not None and len(self.intervals) > 0 __nonzero__ = __bool__
[docs]class TimeInterval(object): """ Time interval object. Thin wrapper around a (start, end) tuple of datetime objects that provides some validation """ _start = None _end = None def __init__(self, start, end): """ Initialise the object with the start and end times :param start: The start time :param end: The end time """ self.start = start self.end = end
[docs] def to_tuple(self): return self.start, self.end
[docs] def validate_types(self, val): if not isinstance(val, (date, datetime)): raise TypeError("start should datetime.datetime object") if self._end is not None and val >= self._end: raise ValueError("start should be < end")
@property def width(self): return self.end - self.start @property def start(self): return self._start @start.setter def start(self, val): self.validate_types(val) self._start = val @property def end(self): return self._end @end.setter def end(self, val): self.validate_types(val) self._end = val def __str__(self): return "({0}, {1}]".format(self.start, self.end) def __repr__(self): return "{}(start={}, end={})".format(self.__class__.__name__, repr(self.start), repr(self.end)) def __eq__(self, other): return isinstance(other, TimeInterval) and self.start == other.start and self.end == other.end def __ne__(self, other): return not self == other def __hash__(self): return hash((self.start, self.end)) def __contains__(self, item): if isinstance(item, (date, datetime)): return self.start < item <= self.end if isinstance(item, TimeInterval): return item.start in self and item.end in self raise TypeError("can't compare datetime.datetime to {}".format(type(item))) def __add__(self, other): if isinstance(other, (tuple, list)) and len(other) == 2: other = RelativeTimeInterval(*other) if not isinstance(other, RelativeTimeInterval): raise ValueError("Can only add a relative time interval to a time interval") return TimeInterval(self.start + other.start, self.end + other.end)
# def resize(self, *args): # if len(args) == 1: # if isinstance(args[0], RelativeTimeInterval): # rti = args[0] # else: # raise TypeError("Single argument should be RelativeTimeInterval") # elif len(args) == 2: # rti = RelativeTimeInterval(*args) # else: # raise ValueError("Too many input arguments") # return self + rti
[docs]class RelativeTimeInterval(TimeInterval): """ Relative time interval object. Thin wrapper around a (start, end) tuple of timedelta objects that provides some validation """ def __init__(self, start, end): """ Initialise the object with the start and end times :param start: The start time :param end: The end time """ start = get_timedelta(start) end = get_timedelta(end) if start >= end: raise ValueError("start should be strictly less than end") if end > timedelta(0): raise ValueError("relative time intervals in the future are not supported") super(RelativeTimeInterval, self).__init__(start, end)
[docs] def validate_types(self, val): if not isinstance(val, timedelta): raise TypeError("start should datetime.timedelta object") if self._end is not None and val >= self._end: raise ValueError("start should be < end")
[docs]def parse_time_tuple(start, end): """ Parse a time tuple. These can be: relative in seconds, e.g. (-4, 0) relative in timedelta, e.g. (timedelta(seconds=-4), timedelta(0)) absolute in date/datetime, e.g. (datetime(2016, 4, 28, 20, 0, 0, 0, UTC), datetime(2016, 4, 28, 21, 0, 0, 0, UTC)) absolute in iso strings, e.g. ("2016-04-28T20:00:00.000Z", "2016-04-28T20:01:00.000Z") Mixtures of relative and absolute are not allowed :param start: Start time :param end: End time :type start: int | timedelta | datetime | str :type end: int | timedelta | datetime | str :return: TimeInterval or RelativeTimeInterval object """ if isinstance(start, int): start_time = timedelta(seconds=start) elif isinstance(start, timedelta): start_time = start elif start is None: start_time = MIN_DATE elif isinstance(start, (date, datetime)): start_time = start.replace(tzinfo=UTC) else: start_time = parse(start).replace(tzinfo=UTC) if isinstance(end, int): # TODO: add check for future (negative values) and ensure that start < end if not isinstance(start_time, timedelta): raise ValueError("Can't mix relative and absolute times") end_time = timedelta(seconds=end) elif isinstance(end, timedelta): if not isinstance(start_time, timedelta): raise ValueError("Can't mix relative and absolute times") end_time = end elif end is None: end_time = utcnow() # TODO: or MAX_DATE? elif isinstance(end, datetime): end_time = end.replace(tzinfo=UTC) else: end_time = parse(end).replace(tzinfo=UTC) if isinstance(start_time, timedelta): return RelativeTimeInterval(start=start_time, end=end_time) else: return TimeInterval(start=start_time, end=end_time)