pyintervals 1.2.2__tar.gz → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyintervals
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Efficient interval operations.
5
5
  License: MIT
6
6
  Keywords: interval,timespan
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyintervals"
3
- version = "1.2.2"
3
+ version = "1.3.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 = 79
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 = 79
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: Interval, other: 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,190 @@
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
+
109
+ def __init__(
110
+ self,
111
+ intervals: Iterable[Interval] = [],
112
+ tz: ZoneInfo | timezone | None = None,
113
+ ):
114
+ self._initialize(tz)
115
+ self.add(intervals)
116
+
117
+ def _initialize(self, tz: ZoneInfo | timezone | None) -> None:
118
+ self.__intervals = list()
119
+ self.__projection_graph = SortedList([TimeValueNode(time_point=TIME_ZERO.replace(tzinfo=tz))])
120
+ self._tz = tz
121
+
122
+ @property
123
+ def intervals(self) -> list[Interval]:
124
+ return list(self.__intervals)
125
+
126
+ def __add__(self, other: IntervalHandler) -> IntervalHandler:
127
+ return _operate(self, other, operand=operator.add)
128
+
129
+ def __iadd__(self, other: IntervalHandler) -> None:
130
+ simplified = _operate(self, other, operand=operator.add)
131
+ self._initialize(tz=self._tz)
132
+ self.add(intervals=simplified.intervals)
133
+ return None
134
+
135
+ def __sub__(self, other: IntervalHandler) -> IntervalHandler:
136
+ return _operate(self, other, operand=operator.sub)
137
+
138
+ def __isub__(self, other: IntervalHandler) -> None:
139
+ simplified = _operate(self, other, operand=operator.sub)
140
+ self._initialize(tz=self._tz)
141
+ self.add(intervals=simplified.intervals)
142
+ return None
143
+
144
+ def __mul__(self, other: IntervalHandler) -> IntervalHandler:
145
+ return _operate(self, other, operand=operator.mul)
146
+
147
+ def __imul__(self, other: IntervalHandler) -> None:
148
+ simplified = _operate(self, other, operand=operator.mul)
149
+ self._initialize(tz=self._tz)
150
+ self.add(intervals=simplified.intervals)
151
+ return None
152
+
153
+ def __truediv__(self, other: IntervalHandler) -> IntervalHandler:
154
+ return _operate(self, other, operand=operator.truediv)
155
+
156
+ def __itruediv__(self, other: IntervalHandler) -> None:
157
+ simplified = _operate(self, other, operand=operator.truediv)
158
+ self._initialize(tz=self._tz)
159
+ self.add(intervals=simplified.intervals)
160
+ return None
161
+
162
+ def add(self, intervals: Iterable[Interval]) -> None:
163
+ """Adds without simplifying the intervals."""
164
+ self.__intervals.extend(intervals)
165
+ for interval in intervals:
166
+ _make_range(self.__projection_graph, interval)
167
+ for node in _relevant_nodes(self.__projection_graph, interval):
168
+ node._add_interval(interval)
169
+
170
+ def remove(self, intervals: Collection[Interval]) -> None:
171
+ """Removes without simplifying the intervals."""
172
+ self.__intervals = [i for i in self.__intervals if i not in intervals]
173
+
174
+ for interval in intervals:
175
+ for node in _relevant_nodes(self.__projection_graph, interval):
176
+ node._remove_interval(interval)
177
+
178
+ self.__projection_graph = SortedList(_simplify(self.__projection_graph))
179
+
180
+ def projection_graph(self) -> SortedList[TimeValueNode]:
181
+ return SortedList(self.__projection_graph)
182
+
183
+ def node_at_time(self, when: datetime) -> TimeValueNode:
184
+ return _active_node_at_time(self.__projection_graph, when)
185
+
186
+ def value_at_time(self, when: datetime) -> float:
187
+ return _active_node_at_time(self.__projection_graph, when).value
188
+
189
+ def get_area(self, during: Interval) -> timedelta:
190
+ return _area_during_interval(self, during)
@@ -1,23 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from bisect import bisect_left
4
- from typing import Sequence, TypeVar, cast
4
+ from typing import TYPE_CHECKING, Sequence, Union
5
5
 
6
- T = TypeVar("T")
6
+ if TYPE_CHECKING:
7
+ from _typeshed import SupportsRichComparisonT
7
8
 
8
9
 
9
- def weak_predecessor(sorted_sequence: Sequence[T], point: T) -> T | None:
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) # type: ignore
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 cast(T, sorted_sequence[insertion_point - 1])
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 cast(T, sorted_sequence[insertion_point - 1])
31
+ return sorted_sequence[insertion_point - 1]
29
32
 
30
33
  # Otherwise, we found the correct point
31
- return cast(T, current)
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
- default_factory=SortedList
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