Coverage for python/lsst/daf/butler/core/timespan.py : 29%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21from __future__ import annotations
23__all__ = ("Timespan", "TIMESPAN_FIELD_SPECS", "TIMESPAN_MIN", "TIMESPAN_MAX")
25import operator
26from typing import Any, Generic, Optional, TypeVar
28from . import ddl, time_utils
31TIMESPAN_MIN = time_utils.EPOCH
32TIMESPAN_MAX = time_utils.MAX_TIME
34T = TypeVar("T")
37class Timespan(Generic[T], tuple):
38 """A generic 2-element named tuple for time intervals.
40 Parameters
41 ----------
42 begin : ``T``, optional
43 Minimum timestamp in the interval (inclusive). `None` is interpreted
44 as -infinity.
45 end : ``T``, optional
46 Maximum timestamp in the interval (inclusive). `None` is interpreted
47 as +infinity.
49 Notes
50 -----
51 This class is generic because it is used for both Python literals (with
52 ``T == astropy.time.Time``) and timestamps in SQLAlchemy expressions
53 (with ``T == sqlalchemy.sql.ColumnElement``), including operations between
54 those.
56 Python's built-in `collections.namedtuple` is not actually a type (just
57 a factory for types), and `typing.NamedTuple` doesn't support generics,
58 so neither can be used here (but also wouldn't add much even if they
59 could).
60 """
61 def __new__(cls, begin: Optional[T], end: Optional[T]):
62 return tuple.__new__(cls, (begin, end))
64 def overlaps(self, other, ops=operator) -> Any:
65 """Test whether this timespan overlaps another.
67 Parameters
68 ----------
69 other : `Timespan`
70 Another timespan whose begin and end values can be compared with
71 those of ``self`` with the ``>=`` operator, yielding values
72 that can be passed to ``ops.or_`` and/or ``ops.and_``.
73 ops : `Any`, optional
74 Any object with ``and_`` and ``or_`` boolean operators. Defaults
75 to the Python built-in `operator` module, which is appropriate when
76 ``T`` is a Python literal like `astropy.time.Time`. When either
77 operand contains SQLAlchemy column expressions, the
78 `sqlalchemy.sql` module should be used instead.
80 Returns
81 -------
82 overlaps : `Any`
83 The result of the overlap. When ``ops`` is `operator`, this will
84 be a `bool`. If ``ops`` is `sqlachemy.sql`, it will be a boolean
85 column expression.
86 """
87 return ops.and_(
88 ops.or_(self.end == None, other.begin == None, self.end >= other.begin), # noqa: E711
89 ops.or_(self.begin == None, other.end == None, other.end >= self.begin), # noqa: E711
90 )
92 def intersection(*args) -> Optional[Timespan]:
93 """Return a new `Timespan` that is contained by all of the given ones.
95 Parameters
96 ----------
97 *args
98 All positional arguments are `Timespan` instances.
100 Returns
101 -------
102 intersection : `Timespan` or `None`
103 The intersection timespan, or `None`, if there is no intersection
104 or no arguments.
106 Notes
107 -----
108 Unlike `overlaps`, this method does not support SQLAlchemy column
109 expressions as operands.
110 """
111 if len(args) == 0:
112 return None
113 elif len(args) == 1:
114 return args[0]
115 else:
116 begins = [ts.begin for ts in args if ts.begin is not None]
117 ends = [ts.end for ts in args if ts.end is not None]
118 if not begins:
119 begin = None
120 elif len(begins) == 1:
121 begin = begins[0]
122 else:
123 begin = max(*begins)
124 if not ends:
125 end = None
126 elif len(ends) == 1:
127 end = ends[0]
128 else:
129 end = min(*ends) if ends else None
130 if begin is not None and end is not None and begin > end:
131 return None
132 return Timespan(begin=begin, end=end)
134 @property
135 def begin(self) -> Optional[T]:
136 """Minimum timestamp in the interval (inclusive).
138 `None` should be interpreted as -infinity.
139 """
140 return self[0]
142 @property
143 def end(self) -> T:
144 """Maximum timestamp in the interval (inclusive).
146 `None` should be interpreted as +infinity.
147 """
148 return self[1]
150 def __getnewargs__(self) -> tuple:
151 return (self.begin, self.end)
154# For timestamps we use Unix time in nanoseconds in TAI scale which need
155# 64-bit integer,
156TIMESPAN_FIELD_SPECS = Timespan(
157 begin=ddl.FieldSpec(name="datetime_begin", dtype=ddl.AstropyTimeNsecTai),
158 end=ddl.FieldSpec(name="datetime_end", dtype=ddl.AstropyTimeNsecTai),
159)