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

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 BaseTimespan(Generic[T], tuple):
38 """A generic 2-element named tuple for time intervals.
40 For cases where either or both bounds may be `None` to represent
41 infinite, the `Timespan` specialization should be preferred over using
42 this class directly.
44 Parameters
45 ----------
46 begin : ``T``
47 Minimum timestamp in the interval (inclusive). `None` is interpreted
48 as -infinity (if allowed by ``T``).
49 end : ``T``
50 Maximum timestamp in the interval (inclusive). `None` is interpreted
51 as +infinity (if allowed by ``T``).
53 Notes
54 -----
55 This class is generic because it is used for both Python literals (with
56 ``T == astropy.time.Time``) and timestamps in SQLAlchemy expressions
57 (with ``T == sqlalchemy.sql.ColumnElement``), including operations between
58 those.
60 Python's built-in `collections.namedtuple` is not actually a type (just
61 a factory for types), and `typing.NamedTuple` doesn't support generics,
62 so neither can be used here (but also wouldn't add much even if they
63 could).
64 """
66 def __new__(cls, begin: T, end: T) -> BaseTimespan:
67 return tuple.__new__(cls, (begin, end))
69 @property
70 def begin(self) -> T:
71 """Minimum timestamp in the interval (inclusive).
73 `None` should be interpreted as -infinity.
74 """
75 return self[0]
77 @property
78 def end(self) -> T:
79 """Maximum timestamp in the interval (inclusive).
81 `None` should be interpreted as +infinity.
82 """
83 return self[1]
85 def __getnewargs__(self) -> tuple:
86 return (self.begin, self.end)
89class Timespan(BaseTimespan[Optional[T]]):
90 """A generic 2-element named tuple for time intervals.
92 `Timespan` explicitly marks both its start and end bounds as possibly
93 `None` (signifying infinite bounds), and provides operations that take
94 that into account.
95 """
97 def overlaps(self, other: Timespan[Any], ops: Any = operator) -> Any:
98 """Test whether this timespan overlaps another.
100 Parameters
101 ----------
102 other : `Timespan`
103 Another timespan whose begin and end values can be compared with
104 those of ``self`` with the ``>=`` operator, yielding values
105 that can be passed to ``ops.or_`` and/or ``ops.and_``.
106 ops : `Any`, optional
107 Any object with ``and_`` and ``or_`` boolean operators. Defaults
108 to the Python built-in `operator` module, which is appropriate when
109 ``T`` is a Python literal like `astropy.time.Time`. When either
110 operand contains SQLAlchemy column expressions, the
111 `sqlalchemy.sql` module should be used instead.
113 Returns
114 -------
115 overlaps : `Any`
116 The result of the overlap. When ``ops`` is `operator`, this will
117 be a `bool`. If ``ops`` is `sqlachemy.sql`, it will be a boolean
118 column expression.
119 """
120 # Silence flake8 below because we use "== None" to invoke SQLAlchemy
121 # operator overloads.
122 # Silence mypy below because this whole method is very much dynamically
123 # typed.
124 return ops.and_(
125 ops.or_(
126 self.end == None, # noqa: E711
127 other.begin == None, # noqa: E711
128 self.end >= other.begin, # type: ignore
129 ),
130 ops.or_(
131 self.begin == None, # noqa: E711
132 other.end == None, # noqa: E711
133 other.end >= self.begin, # type: ignore
134 ),
135 )
137 def intersection(*args: Timespan[Any]) -> Optional[Timespan]:
138 """Return a new `Timespan` that is contained by all of the given ones.
140 Parameters
141 ----------
142 *args
143 All positional arguments are `Timespan` instances.
145 Returns
146 -------
147 intersection : `Timespan` or `None`
148 The intersection timespan, or `None`, if there is no intersection
149 or no arguments.
151 Notes
152 -----
153 Unlike `overlaps`, this method does not support SQLAlchemy column
154 expressions as operands.
155 """
156 if len(args) == 0:
157 return None
158 elif len(args) == 1:
159 return args[0]
160 else:
161 begins = [ts.begin for ts in args if ts.begin is not None]
162 ends = [ts.end for ts in args if ts.end is not None]
163 if not begins:
164 begin = None
165 elif len(begins) == 1:
166 begin = begins[0]
167 else:
168 begin = max(*begins)
169 if not ends:
170 end = None
171 elif len(ends) == 1:
172 end = ends[0]
173 else:
174 end = min(*ends) if ends else None
175 if begin is not None and end is not None and begin > end:
176 return None
177 return Timespan(begin=begin, end=end)
180# For timestamps we use Unix time in nanoseconds in TAI scale which need
181# 64-bit integer,
182TIMESPAN_FIELD_SPECS: BaseTimespan[ddl.FieldSpec] = BaseTimespan(
183 begin=ddl.FieldSpec(name="datetime_begin", dtype=ddl.AstropyTimeNsecTai),
184 end=ddl.FieldSpec(name="datetime_end", dtype=ddl.AstropyTimeNsecTai),
185)