pyintervals 1.2.2__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyintervals-1.2.2 → pyintervals-1.4.0}/PKG-INFO +1 -1
- {pyintervals-1.2.2 → pyintervals-1.4.0}/pyproject.toml +3 -3
- {pyintervals-1.2.2 → pyintervals-1.4.0}/src/pyintervals/interval.py +4 -14
- pyintervals-1.4.0/src/pyintervals/interval_handler.py +208 -0
- {pyintervals-1.2.2 → pyintervals-1.4.0}/src/pyintervals/search.py +11 -8
- {pyintervals-1.2.2 → pyintervals-1.4.0}/src/pyintervals/time_value_node.py +5 -18
- pyintervals-1.2.2/src/pyintervals/interval_handler.py +0 -129
- {pyintervals-1.2.2 → pyintervals-1.4.0}/CHANGELOG.rst +0 -0
- {pyintervals-1.2.2 → pyintervals-1.4.0}/LICENSE +0 -0
- {pyintervals-1.2.2 → pyintervals-1.4.0}/README.rst +0 -0
- {pyintervals-1.2.2 → pyintervals-1.4.0}/src/pyintervals/__init__.py +0 -0
- {pyintervals-1.2.2 → pyintervals-1.4.0}/src/pyintervals/constants.py +0 -0
- {pyintervals-1.2.2 → pyintervals-1.4.0}/src/pyintervals/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pyintervals"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0"
|
|
4
4
|
description = "Efficient interval operations."
|
|
5
5
|
authors = ["Serkan Kalay <serkanosmankalay@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -47,7 +47,7 @@ isort = "^5.7.0"
|
|
|
47
47
|
tox = ">=3.26,<5.0"
|
|
48
48
|
|
|
49
49
|
[tool.black]
|
|
50
|
-
line-length =
|
|
50
|
+
line-length = 120
|
|
51
51
|
include = '\.pyi?$'
|
|
52
52
|
exclude = '''
|
|
53
53
|
/(
|
|
@@ -63,7 +63,7 @@ exclude = '''
|
|
|
63
63
|
'''
|
|
64
64
|
|
|
65
65
|
[tool.isort]
|
|
66
|
-
line_length =
|
|
66
|
+
line_length = 120
|
|
67
67
|
profile = 'black'
|
|
68
68
|
add_imports = ['from __future__ import annotations']
|
|
69
69
|
|
|
@@ -12,9 +12,7 @@ class Interval:
|
|
|
12
12
|
|
|
13
13
|
def __post_init__(self) -> None:
|
|
14
14
|
if self.start > self.end:
|
|
15
|
-
raise RuntimeError(
|
|
16
|
-
f"Invalid interval: {self.end=} is earlier than {self.start=}"
|
|
17
|
-
)
|
|
15
|
+
raise RuntimeError(f"Invalid interval: {self.end=} is earlier than {self.start=}")
|
|
18
16
|
|
|
19
17
|
def is_degenerate(self) -> bool:
|
|
20
18
|
return self.start == self.end
|
|
@@ -29,14 +27,8 @@ class Interval:
|
|
|
29
27
|
return contains(self, other)
|
|
30
28
|
|
|
31
29
|
|
|
32
|
-
def _get_ordered(
|
|
33
|
-
interval
|
|
34
|
-
) -> tuple[Interval, Interval]:
|
|
35
|
-
return (
|
|
36
|
-
(interval, other)
|
|
37
|
-
if interval.start <= other.start
|
|
38
|
-
else (other, interval)
|
|
39
|
-
)
|
|
30
|
+
def _get_ordered(interval: Interval, other: Interval) -> tuple[Interval, Interval]:
|
|
31
|
+
return (interval, other) if interval.start <= other.start else (other, interval)
|
|
40
32
|
|
|
41
33
|
|
|
42
34
|
def contains_point(interval: Interval, point: datetime) -> bool:
|
|
@@ -75,9 +67,7 @@ def contains(interval: Interval, other: Interval) -> bool:
|
|
|
75
67
|
return interval.start <= other.start and interval.end >= other.end
|
|
76
68
|
|
|
77
69
|
|
|
78
|
-
def intersection(
|
|
79
|
-
interval: Interval, other_interval: Interval
|
|
80
|
-
) -> Interval | None:
|
|
70
|
+
def intersection(interval: Interval, other_interval: Interval) -> Interval | None:
|
|
81
71
|
if overlaps(interval, other_interval):
|
|
82
72
|
return Interval(
|
|
83
73
|
max([interval.start, other_interval.start]),
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import operator
|
|
5
|
+
from collections.abc import Callable, Collection, Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from zoneinfo import ZoneInfo
|
|
9
|
+
|
|
10
|
+
import more_itertools
|
|
11
|
+
from sortedcontainers import SortedList
|
|
12
|
+
|
|
13
|
+
from .constants import TIME_ZERO
|
|
14
|
+
from .interval import Interval
|
|
15
|
+
from .search import weak_predecessor
|
|
16
|
+
from .time_value_node import TimeValueNode, _simplify
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _to_new_node(
|
|
20
|
+
active_node: TimeValueNode | None,
|
|
21
|
+
time_point: datetime,
|
|
22
|
+
) -> TimeValueNode | None:
|
|
23
|
+
if active_node is None:
|
|
24
|
+
return TimeValueNode(time_point)
|
|
25
|
+
|
|
26
|
+
return TimeValueNode.clone(active_node, time_point) if active_node.time_point < time_point else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _active_node_at_time(nodes: SortedList[TimeValueNode], when: datetime) -> TimeValueNode:
|
|
30
|
+
if node := weak_predecessor(nodes, TimeValueNode(when)):
|
|
31
|
+
return node
|
|
32
|
+
else:
|
|
33
|
+
raise RuntimeError("Could not find active node at time.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _make_range(nodes: SortedList[TimeValueNode], new_interval: Interval) -> None:
|
|
37
|
+
for t in {new_interval.start, new_interval.end}:
|
|
38
|
+
if new_node := _to_new_node(
|
|
39
|
+
active_node=_active_node_at_time(nodes, t),
|
|
40
|
+
time_point=t,
|
|
41
|
+
):
|
|
42
|
+
nodes.add(new_node)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _operate(
|
|
46
|
+
a: IntervalHandler,
|
|
47
|
+
b: IntervalHandler,
|
|
48
|
+
operand: Callable[[float, float], float],
|
|
49
|
+
) -> IntervalHandler:
|
|
50
|
+
"""Only call this function through the methods bound to `IntervalHandler`."""
|
|
51
|
+
if not isinstance(b, IntervalHandler):
|
|
52
|
+
raise TypeError(f"unsupported operand type(s) for {operand.__name__}: " f"'{type(a)}' and '{type(b)}'")
|
|
53
|
+
change_times = set(n.time_point for n in itertools.chain(a.projection_graph, b.projection_graph))
|
|
54
|
+
|
|
55
|
+
return IntervalHandler(
|
|
56
|
+
intervals=[
|
|
57
|
+
Interval(
|
|
58
|
+
start=start,
|
|
59
|
+
end=end,
|
|
60
|
+
value=operand(a.value_at_time(start), b.value_at_time(start)),
|
|
61
|
+
)
|
|
62
|
+
for start, end in more_itertools.pairwise(sorted(change_times))
|
|
63
|
+
],
|
|
64
|
+
tz=a._tz,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _relevant_nodes(
|
|
69
|
+
nodes: SortedList[TimeValueNode],
|
|
70
|
+
interval: Interval,
|
|
71
|
+
) -> list[TimeValueNode]:
|
|
72
|
+
if interval.is_degenerate():
|
|
73
|
+
return [_active_node_at_time(nodes, interval.start)]
|
|
74
|
+
else:
|
|
75
|
+
return [
|
|
76
|
+
n
|
|
77
|
+
for n in nodes.islice(start=nodes.index(_active_node_at_time(nodes, interval.start)))
|
|
78
|
+
if n.time_point <= interval.end
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _area_during_interval(handler: IntervalHandler, during: Interval) -> timedelta:
|
|
83
|
+
first_node_in_interval = TimeValueNode.clone(handler.node_at_time(during.start), during.start)
|
|
84
|
+
last_node_in_interval = TimeValueNode.clone(handler.node_at_time(during.end), during.end)
|
|
85
|
+
|
|
86
|
+
relevant_nodes = itertools.chain(
|
|
87
|
+
[first_node_in_interval],
|
|
88
|
+
# ↓ Everything except the first node, since it's not ↓
|
|
89
|
+
# ↓ necessarily part of the interval ↓
|
|
90
|
+
_relevant_nodes(handler.projection_graph, during)[1:],
|
|
91
|
+
[last_node_in_interval],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return sum(
|
|
95
|
+
(
|
|
96
|
+
during.value * start.value * (end.time_point - start.time_point)
|
|
97
|
+
for start, end in more_itertools.pairwise(relevant_nodes)
|
|
98
|
+
),
|
|
99
|
+
start=timedelta(0),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class IntervalHandler:
|
|
105
|
+
__intervals: list[Interval]
|
|
106
|
+
__projection_graph: SortedList[TimeValueNode]
|
|
107
|
+
_tz: ZoneInfo | timezone | None
|
|
108
|
+
__first_negative: TimeValueNode | None = None
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
intervals: Iterable[Interval] = [],
|
|
113
|
+
tz: ZoneInfo | timezone | None = None,
|
|
114
|
+
):
|
|
115
|
+
self._initialize(tz)
|
|
116
|
+
self.add(intervals)
|
|
117
|
+
|
|
118
|
+
def _initialize(self, tz: ZoneInfo | timezone | None) -> None:
|
|
119
|
+
self.__intervals = list()
|
|
120
|
+
self.__projection_graph = SortedList([TimeValueNode(time_point=TIME_ZERO.replace(tzinfo=tz))])
|
|
121
|
+
self._tz = tz
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def intervals(self) -> list[Interval]:
|
|
125
|
+
return list(self.__intervals)
|
|
126
|
+
|
|
127
|
+
def __add__(self, other: IntervalHandler) -> IntervalHandler:
|
|
128
|
+
return _operate(self, other, operand=operator.add)
|
|
129
|
+
|
|
130
|
+
def __iadd__(self, other: IntervalHandler) -> None:
|
|
131
|
+
simplified = _operate(self, other, operand=operator.add)
|
|
132
|
+
self._initialize(tz=self._tz)
|
|
133
|
+
self.add(intervals=simplified.intervals)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def __sub__(self, other: IntervalHandler) -> IntervalHandler:
|
|
137
|
+
return _operate(self, other, operand=operator.sub)
|
|
138
|
+
|
|
139
|
+
def __isub__(self, other: IntervalHandler) -> None:
|
|
140
|
+
simplified = _operate(self, other, operand=operator.sub)
|
|
141
|
+
self._initialize(tz=self._tz)
|
|
142
|
+
self.add(intervals=simplified.intervals)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def __mul__(self, other: IntervalHandler) -> IntervalHandler:
|
|
146
|
+
return _operate(self, other, operand=operator.mul)
|
|
147
|
+
|
|
148
|
+
def __imul__(self, other: IntervalHandler) -> None:
|
|
149
|
+
simplified = _operate(self, other, operand=operator.mul)
|
|
150
|
+
self._initialize(tz=self._tz)
|
|
151
|
+
self.add(intervals=simplified.intervals)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def __truediv__(self, other: IntervalHandler) -> IntervalHandler:
|
|
155
|
+
return _operate(self, other, operand=operator.truediv)
|
|
156
|
+
|
|
157
|
+
def __itruediv__(self, other: IntervalHandler) -> None:
|
|
158
|
+
simplified = _operate(self, other, operand=operator.truediv)
|
|
159
|
+
self._initialize(tz=self._tz)
|
|
160
|
+
self.add(intervals=simplified.intervals)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def add(self, intervals: Iterable[Interval]) -> None:
|
|
164
|
+
"""Adds without simplifying the intervals."""
|
|
165
|
+
self.__intervals.extend(intervals)
|
|
166
|
+
for interval in intervals:
|
|
167
|
+
_make_range(self.__projection_graph, interval)
|
|
168
|
+
for node in _relevant_nodes(self.__projection_graph, interval):
|
|
169
|
+
node._add_interval(interval)
|
|
170
|
+
self._try_refresh_first_negative_point(node)
|
|
171
|
+
|
|
172
|
+
def remove(self, intervals: Collection[Interval]) -> None:
|
|
173
|
+
"""Removes without simplifying the intervals."""
|
|
174
|
+
self.__intervals = [i for i in self.__intervals if i not in intervals]
|
|
175
|
+
|
|
176
|
+
for interval in intervals:
|
|
177
|
+
for node in _relevant_nodes(self.__projection_graph, interval):
|
|
178
|
+
node._remove_interval(interval)
|
|
179
|
+
self._try_refresh_first_negative_point(node)
|
|
180
|
+
|
|
181
|
+
self.__projection_graph = SortedList(_simplify(self.__projection_graph))
|
|
182
|
+
if self.__first_negative is None:
|
|
183
|
+
self.__first_negative = next((n for n in self.__projection_graph if n.value < 0), None)
|
|
184
|
+
|
|
185
|
+
def _try_refresh_first_negative_point(self, node: TimeValueNode) -> None:
|
|
186
|
+
if node.value < 0:
|
|
187
|
+
if self.__first_negative is None or node.time_point < self.__first_negative.time_point:
|
|
188
|
+
self.__first_negative = node
|
|
189
|
+
|
|
190
|
+
if self.__first_negative and self.__first_negative.value >= 0:
|
|
191
|
+
self.__first_negative = None
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def projection_graph(self) -> SortedList[TimeValueNode]:
|
|
195
|
+
return SortedList(self.__projection_graph)
|
|
196
|
+
|
|
197
|
+
def node_at_time(self, when: datetime) -> TimeValueNode:
|
|
198
|
+
return _active_node_at_time(self.__projection_graph, when)
|
|
199
|
+
|
|
200
|
+
def value_at_time(self, when: datetime) -> float:
|
|
201
|
+
return _active_node_at_time(self.__projection_graph, when).value
|
|
202
|
+
|
|
203
|
+
def get_area(self, during: Interval) -> timedelta:
|
|
204
|
+
return _area_during_interval(self, during)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def first_negative_point(self) -> TimeValueNode | None:
|
|
208
|
+
return self.__first_negative
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from bisect import bisect_left
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING, Sequence, Union
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from _typeshed import SupportsRichComparisonT
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def weak_predecessor(
|
|
10
|
+
def weak_predecessor(
|
|
11
|
+
sorted_sequence: Sequence[SupportsRichComparisonT], point: SupportsRichComparisonT
|
|
12
|
+
) -> Union[SupportsRichComparisonT, None]:
|
|
10
13
|
# If the passed sequence is empty, then no predecessor.
|
|
11
14
|
if not sorted_sequence:
|
|
12
15
|
return None
|
|
13
16
|
|
|
14
|
-
insertion_point = bisect_left(sorted_sequence, point)
|
|
17
|
+
insertion_point = bisect_left(sorted_sequence, point)
|
|
15
18
|
if insertion_point == len(sorted_sequence):
|
|
16
19
|
# Then, we need to insert to the end of the collection.
|
|
17
20
|
# Hence, the weak predecessor is the last element in the list.
|
|
18
|
-
return
|
|
21
|
+
return sorted_sequence[insertion_point - 1]
|
|
19
22
|
|
|
20
|
-
if (current := sorted_sequence[insertion_point]) and current > point:
|
|
23
|
+
if (current := sorted_sequence[insertion_point]) and current > point: # type: ignore[operator]
|
|
21
24
|
# Then, we found a point which is the successor of our reference.
|
|
22
25
|
# Hence, return the predecessor.
|
|
23
26
|
|
|
@@ -25,7 +28,7 @@ def weak_predecessor(sorted_sequence: Sequence[T], point: T) -> T | None:
|
|
|
25
28
|
if insertion_point == 0:
|
|
26
29
|
return None
|
|
27
30
|
else:
|
|
28
|
-
return
|
|
31
|
+
return sorted_sequence[insertion_point - 1]
|
|
29
32
|
|
|
30
33
|
# Otherwise, we found the correct point
|
|
31
|
-
return
|
|
34
|
+
return current
|
|
@@ -16,12 +16,8 @@ from pyintervals.interval import contains_point
|
|
|
16
16
|
class TimeValueNode:
|
|
17
17
|
time_point: datetime
|
|
18
18
|
__intervals: SortedList[Interval] = field(default_factory=SortedList)
|
|
19
|
-
__starting_intervals: SortedList[Interval] = field(
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
__ending_intervals: SortedList[Interval] = field(
|
|
23
|
-
default_factory=SortedList
|
|
24
|
-
)
|
|
19
|
+
__starting_intervals: SortedList[Interval] = field(default_factory=SortedList)
|
|
20
|
+
__ending_intervals: SortedList[Interval] = field(default_factory=SortedList)
|
|
25
21
|
|
|
26
22
|
@property
|
|
27
23
|
def intervals(self) -> list[Interval]:
|
|
@@ -40,19 +36,12 @@ class TimeValueNode:
|
|
|
40
36
|
return sum(i.value for i in self.__intervals if not i.is_degenerate())
|
|
41
37
|
|
|
42
38
|
def is_redundant(self) -> bool:
|
|
43
|
-
return
|
|
44
|
-
self.time_point > TIME_ZERO
|
|
45
|
-
and not self.__starting_intervals
|
|
46
|
-
and not self.__ending_intervals
|
|
47
|
-
)
|
|
39
|
+
return self.time_point > TIME_ZERO and not self.__starting_intervals and not self.__ending_intervals
|
|
48
40
|
|
|
49
41
|
def __eq__(self, other: object) -> bool:
|
|
50
42
|
if not isinstance(other, TimeValueNode):
|
|
51
43
|
raise NotImplementedError
|
|
52
|
-
return
|
|
53
|
-
self.time_point == other.time_point
|
|
54
|
-
and self.__intervals == other.__intervals
|
|
55
|
-
)
|
|
44
|
+
return self.time_point == other.time_point and self.__intervals == other.__intervals
|
|
56
45
|
|
|
57
46
|
def __ne__(self, other: object) -> bool:
|
|
58
47
|
return not (self == other)
|
|
@@ -97,9 +86,7 @@ class TimeValueNode:
|
|
|
97
86
|
self.__ending_intervals.remove(interval)
|
|
98
87
|
|
|
99
88
|
@staticmethod
|
|
100
|
-
def clone(
|
|
101
|
-
given: TimeValueNode, to_time: datetime | None = None
|
|
102
|
-
) -> TimeValueNode:
|
|
89
|
+
def clone(given: TimeValueNode, to_time: datetime | None = None) -> TimeValueNode:
|
|
103
90
|
return (
|
|
104
91
|
TimeValueNode(given.time_point, SortedList(given.__intervals))
|
|
105
92
|
if not to_time
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from typing import Collection, Iterable, Sequence
|
|
6
|
-
from zoneinfo import ZoneInfo
|
|
7
|
-
|
|
8
|
-
from sortedcontainers import SortedList
|
|
9
|
-
|
|
10
|
-
from .constants import TIME_ZERO
|
|
11
|
-
from .interval import Interval, intersection
|
|
12
|
-
from .search import weak_predecessor
|
|
13
|
-
from .time_value_node import TimeValueNode, _simplify
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _to_new_node(
|
|
17
|
-
active_node: TimeValueNode | None,
|
|
18
|
-
time_point: datetime,
|
|
19
|
-
) -> TimeValueNode | None:
|
|
20
|
-
if active_node is None:
|
|
21
|
-
return TimeValueNode(time_point)
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
TimeValueNode.clone(active_node, time_point)
|
|
25
|
-
if active_node.time_point < time_point
|
|
26
|
-
else None
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _active_node_at_time(
|
|
31
|
-
nodes: SortedList[TimeValueNode], when: datetime
|
|
32
|
-
) -> TimeValueNode:
|
|
33
|
-
if node := weak_predecessor(nodes, TimeValueNode(when)):
|
|
34
|
-
return node
|
|
35
|
-
else:
|
|
36
|
-
raise RuntimeError("Could not find active node at time.")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _make_range(
|
|
40
|
-
nodes: SortedList[TimeValueNode], new_interval: Interval
|
|
41
|
-
) -> None:
|
|
42
|
-
for t in {new_interval.start, new_interval.end}:
|
|
43
|
-
if new_node := _to_new_node(
|
|
44
|
-
active_node=_active_node_at_time(nodes, t),
|
|
45
|
-
time_point=t,
|
|
46
|
-
):
|
|
47
|
-
nodes.add(new_node)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _relevant_nodes(
|
|
51
|
-
nodes: SortedList[TimeValueNode],
|
|
52
|
-
interval: Interval,
|
|
53
|
-
) -> list[TimeValueNode]:
|
|
54
|
-
if interval.is_degenerate():
|
|
55
|
-
return [_active_node_at_time(nodes, interval.start)]
|
|
56
|
-
else:
|
|
57
|
-
return [
|
|
58
|
-
n
|
|
59
|
-
for n in nodes.islice(
|
|
60
|
-
start=nodes.index(_active_node_at_time(nodes, interval.start))
|
|
61
|
-
)
|
|
62
|
-
if n.time_point <= interval.end
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _area_during_interval(
|
|
67
|
-
handler: IntervalHandler,
|
|
68
|
-
during: Interval,
|
|
69
|
-
) -> timedelta:
|
|
70
|
-
return sum(
|
|
71
|
-
(
|
|
72
|
-
interval.value * during.value * overlap.duration()
|
|
73
|
-
for interval in handler.intervals
|
|
74
|
-
if (overlap := intersection(during, interval))
|
|
75
|
-
),
|
|
76
|
-
start=timedelta(0),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@dataclass
|
|
81
|
-
class IntervalHandler:
|
|
82
|
-
__intervals: list[Interval]
|
|
83
|
-
__projection_graph: SortedList[TimeValueNode]
|
|
84
|
-
|
|
85
|
-
def __init__(
|
|
86
|
-
self, intervals: Iterable[Interval] = [], tz: ZoneInfo | None = None
|
|
87
|
-
):
|
|
88
|
-
self._initialize(tz)
|
|
89
|
-
self.add(intervals)
|
|
90
|
-
|
|
91
|
-
def _initialize(self, tz: ZoneInfo | None) -> None:
|
|
92
|
-
self.__intervals = list()
|
|
93
|
-
self.__projection_graph = SortedList(
|
|
94
|
-
[TimeValueNode(time_point=TIME_ZERO.replace(tzinfo=tz))]
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def intervals(self) -> list[Interval]:
|
|
99
|
-
return list(self.__intervals)
|
|
100
|
-
|
|
101
|
-
def add(self, intervals: Iterable[Interval]) -> None:
|
|
102
|
-
self.__intervals.extend(intervals)
|
|
103
|
-
for interval in intervals:
|
|
104
|
-
_make_range(self.__projection_graph, interval)
|
|
105
|
-
for node in _relevant_nodes(self.__projection_graph, interval):
|
|
106
|
-
node._add_interval(interval)
|
|
107
|
-
|
|
108
|
-
def remove(self, intervals: Collection[Interval]) -> None:
|
|
109
|
-
self.__intervals = [i for i in self.__intervals if i not in intervals]
|
|
110
|
-
|
|
111
|
-
for interval in intervals:
|
|
112
|
-
for node in _relevant_nodes(self.__projection_graph, interval):
|
|
113
|
-
node._remove_interval(interval)
|
|
114
|
-
|
|
115
|
-
self.__projection_graph = SortedList(
|
|
116
|
-
_simplify(self.__projection_graph)
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
def projection_graph(self) -> Sequence[TimeValueNode]:
|
|
120
|
-
return list(self.__projection_graph)
|
|
121
|
-
|
|
122
|
-
def node_at_time(self, when: datetime) -> TimeValueNode:
|
|
123
|
-
return _active_node_at_time(self.__projection_graph, when)
|
|
124
|
-
|
|
125
|
-
def value_at_time(self, when: datetime) -> float:
|
|
126
|
-
return _active_node_at_time(self.__projection_graph, when).value
|
|
127
|
-
|
|
128
|
-
def get_area(self, during: Interval) -> timedelta:
|
|
129
|
-
return _area_during_interval(self, during)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|