Projet

Général

Profil

0001-general-add-custom-implementation-of-interval-sets-2.patch

Frédéric Péters, 18 décembre 2017 22:03

Télécharger (6,83 ko)

Voir les différences:

Subject: [PATCH 1/3] general: add custom implementation of interval sets
 (#20732)

 chrono/interval.py     | 116 +++++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_interval.py |  62 ++++++++++++++++++++++++++
 tox.ini                |   1 +
 3 files changed, 179 insertions(+)
 create mode 100644 chrono/interval.py
 create mode 100644 tests/test_interval.py
chrono/interval.py
1
# chrono - agendas system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import bisect
18

  
19

  
20
class Interval(object):
21
    __slots__ = ['begin', 'end', 'data']
22

  
23
    def __init__(self, begin, end, data=None):
24
        assert begin < end
25
        self.begin = begin
26
        self.end = end
27
        self.data = data
28

  
29
    def overlap(self, begin, end):
30
        if end <= self.begin:
31
            return False
32
        if begin >= self.end:
33
            return False
34
        return True
35

  
36
    def __repr__(self):
37
        return '<Interval [%s, %s] %s>' % (self.begin, self.end, self.data or '')
38

  
39

  
40
class Intervals(object):
41
    "Maintain a list of mostly non overlapping intervals, allow removing overlap"
42
    def __init__(self):
43
        self.points = []
44
        self.container = []
45

  
46
    def __insert_point(self, point, interval):
47
        i = bisect.bisect_left(self.points, point)
48
        if i >= len(self.container) or self.points[i] != point:
49
            self.points.insert(i, point)
50
            self.container.insert(i, [])
51
        self.container[i].append(interval)
52

  
53
    def add(self, begin, end, data=None):
54
        'Add an interval'
55
        self.add_interval(Interval(begin, end, data))
56

  
57
    def add_interval(self, interval):
58
        'Add an interval object'
59
        self.__insert_point(interval.begin, interval)
60
        self.__insert_point(interval.end, interval)
61

  
62
    def __iter_interval(self, begin, end, modify=False):
63
         i = bisect.bisect_left(self.points, begin)
64
         while i < len(self.points) and self.points[i] <= end:
65
             container = self.container[i]
66
             if modify:
67
                 container = list(container)
68
             for interval in container:
69
                 yield self.points[i], interval
70
             i += 1
71

  
72
    def remove_overlap(self, begin, end):
73
        'Remove all overlapping intervals'
74
        for point, interval in self.__iter_interval(begin, end, modify=True):
75
            if interval.overlap(begin, end):
76
                self.__remove_interval(interval)
77

  
78
    def overlap(self, begin, end):
79
        'Test if some intervals overlap'
80
        for point, interval in self.__iter_interval(begin, end):
81
            if interval.overlap(begin, end):
82
                return True
83
        return False
84

  
85
    def search(self, begin, end):
86
        'Search overlapping intervals'
87
        for point, interval in self.__iter_interval(begin, end):
88
            if interval.overlap(begin, end):
89
                 # prevent returning the same interval twice
90
                 if interval.begin < begin or interval.begin == point:
91
                     yield interval
92

  
93
    def search_data(self, begin, end):
94
        'Search data elements of overlapping intervals'
95
        for interval in self.search(begin, end):
96
            yield interval.data
97

  
98
    def iter(self):
99
        'Iterate intervals'
100
        if not self.points:
101
            return []
102
        return self.search(self.points[0], self.points[-1])
103

  
104
    def iter_data(self):
105
        'Iterate data element attached to intervals'
106
        for interval in self.iter():
107
            yield interval.data
108

  
109
    def __remove_interval(self, interval):
110
        self.__remove_point_interval(interval.begin, interval)
111
        self.__remove_point_interval(interval.end, interval)
112

  
113
    def __remove_point_interval(self, point, interval):
114
        i = bisect.bisect_left(self.points, point)
115
        assert self.points[i] == point
116
        self.container[i].remove(interval)
tests/test_interval.py
1
import pytest
2

  
3
try:
4
    from intervaltree import IntervalTree
5
except ImportError:
6
    IntervalTree = None
7

  
8
from chrono.interval import Interval, Intervals
9

  
10

  
11
def test_interval_repr():
12
    a = Interval(1, 4)
13
    repr(a)
14

  
15
def test_interval_overlap():
16
    a = Interval(1, 4)
17

  
18
    assert not a.overlap(0, 1)
19
    assert a.overlap(0, 2)
20
    assert a.overlap(1, 4)
21
    assert a.overlap(2, 3)
22
    assert a.overlap(3, 5)
23
    assert not a.overlap(5, 6)
24

  
25
def test_intervals():
26
    intervals = Intervals()
27

  
28
    assert len(list(intervals.search(0, 5))) == 0
29

  
30
    for i in range(10):
31
        intervals.add(i, i + 1, 1)
32

  
33
    for i in range(10, 20):
34
        intervals.add(i, i + 1, 2)
35

  
36
    for i in range(5, 15):
37
        intervals.add(i, i + 1, 3)
38

  
39
    assert len(list(intervals.search(0, 5))) == 5
40
    assert len(list(intervals.search(0, 10))) == 15
41
    assert len(list(intervals.search(5, 15))) == 20
42
    assert len(list(intervals.search(10, 20))) == 15
43
    assert len(list(intervals.search(15, 20))) == 5
44

  
45
    assert set(intervals.search_data(0, 5)) == {1}
46
    assert set(intervals.search_data(0, 10)) == {1, 3}
47
    assert set(intervals.search_data(5, 15)) == {1, 2, 3}
48
    assert set(intervals.search_data(10, 20)) == {2, 3}
49
    assert set(intervals.search_data(15, 20)) == {2}
50

  
51
    for i in range(20):
52
        assert intervals.overlap(i, i + 1)
53

  
54
    intervals.remove_overlap(5, 15)
55
    assert set(intervals.search_data(0, 20)) == {1, 2}
56

  
57
    for i in range(5):
58
        assert intervals.overlap(i, i + 1)
59
    for i in range(5, 15):
60
        assert not intervals.overlap(i, i + 1)
61
    for i in range(15, 20):
62
        assert intervals.overlap(i, i + 1)
tox.ini
15 15
  django111: django>=1.11,<1.12
16 16
  pytest-cov
17 17
  pytest-django
18
  intervaltree
18 19
  pytest>=3.3.0
19 20
  WebTest
20 21
  mock
21
-