Hide keyboard shortcuts

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 

22 

23__all__ = ("Timespan", "TIMESPAN_FIELD_SPECS", "TIMESPAN_MIN", "TIMESPAN_MAX") 

24 

25import operator 

26from typing import Any, Generic, Optional, TypeVar 

27 

28from . import ddl, time_utils 

29 

30 

31TIMESPAN_MIN = time_utils.EPOCH 

32TIMESPAN_MAX = time_utils.MAX_TIME 

33 

34T = TypeVar("T") 

35 

36 

37class BaseTimespan(Generic[T], tuple): 

38 """A generic 2-element named tuple for time intervals. 

39 

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. 

43 

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``). 

52 

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. 

59 

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 """ 

65 

66 def __new__(cls, begin: T, end: T) -> BaseTimespan: 

67 return tuple.__new__(cls, (begin, end)) 

68 

69 @property 

70 def begin(self) -> T: 

71 """Minimum timestamp in the interval (inclusive). 

72 

73 `None` should be interpreted as -infinity. 

74 """ 

75 return self[0] 

76 

77 @property 

78 def end(self) -> T: 

79 """Maximum timestamp in the interval (inclusive). 

80 

81 `None` should be interpreted as +infinity. 

82 """ 

83 return self[1] 

84 

85 def __getnewargs__(self) -> tuple: 

86 return (self.begin, self.end) 

87 

88 

89class Timespan(BaseTimespan[Optional[T]]): 

90 """A generic 2-element named tuple for time intervals. 

91 

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 """ 

96 

97 def overlaps(self, other: Timespan[Any], ops: Any = operator) -> Any: 

98 """Test whether this timespan overlaps another. 

99 

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. 

112 

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 ) 

136 

137 def intersection(*args: Timespan[Any]) -> Optional[Timespan]: 

138 """Return a new `Timespan` that is contained by all of the given ones. 

139 

140 Parameters 

141 ---------- 

142 *args 

143 All positional arguments are `Timespan` instances. 

144 

145 Returns 

146 ------- 

147 intersection : `Timespan` or `None` 

148 The intersection timespan, or `None`, if there is no intersection 

149 or no arguments. 

150 

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) 

178 

179 

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)