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 Timespan(Generic[T], tuple): 

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

39 

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. 

48 

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. 

55 

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

63 

64 def overlaps(self, other, ops=operator) -> Any: 

65 """Test whether this timespan overlaps another. 

66 

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. 

79 

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 ) 

91 

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

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

94 

95 Parameters 

96 ---------- 

97 *args 

98 All positional arguments are `Timespan` instances. 

99 

100 Returns 

101 ------- 

102 intersection : `Timespan` or `None` 

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

104 or no arguments. 

105 

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) 

133 

134 @property 

135 def begin(self) -> Optional[T]: 

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

137 

138 `None` should be interpreted as -infinity. 

139 """ 

140 return self[0] 

141 

142 @property 

143 def end(self) -> T: 

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

145 

146 `None` should be interpreted as +infinity. 

147 """ 

148 return self[1] 

149 

150 def __getnewargs__(self) -> tuple: 

151 return (self.begin, self.end) 

152 

153 

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)