disjoint_interval_tree 0.1.0
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.
- checksums.yaml +7 -0
- data/CITATION.cff +67 -0
- data/README.md +340 -0
- data/Rakefile +232 -0
- data/disjoint_interval_tree.gemspec +42 -0
- data/ext/disjoint_interval_tree/dit.c +850 -0
- data/ext/disjoint_interval_tree/dit.h +244 -0
- data/ext/disjoint_interval_tree/dit_ruby.c +648 -0
- data/ext/disjoint_interval_tree/extconf.rb +17 -0
- data/lib/disjoint_interval_tree/version.rb +5 -0
- data/lib/disjoint_interval_tree.rb +52 -0
- data/test/c/Makefile +58 -0
- data/test/c/test_dit.c +1072 -0
- data/test/test_disjoint_interval_tree.rb +649 -0
- metadata +65 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2025 INRIA
|
|
5
|
+
#
|
|
6
|
+
# Contributors :
|
|
7
|
+
# Romain PEREIRA, romain.pereira@inria.fr + rpereira@anl.gov
|
|
8
|
+
#
|
|
9
|
+
# This software is governed by the CeCILL-C license under French law and
|
|
10
|
+
# abiding by the rules of distribution of free software. You can use,
|
|
11
|
+
# modify and/ or redistribute the software under the terms of the CeCILL-C
|
|
12
|
+
# license as circulated by CEA, CNRS and INRIA at the following URL
|
|
13
|
+
# "http://www.cecill.info".
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
require 'minitest/autorun'
|
|
17
|
+
require 'disjoint_interval_tree'
|
|
18
|
+
|
|
19
|
+
# A naive reference implementation, used to cross-check the tree.
|
|
20
|
+
class IntervalModel
|
|
21
|
+
attr_reader :intervals
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@intervals = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def intersect(a, b)
|
|
28
|
+
return [] if a >= b
|
|
29
|
+
|
|
30
|
+
@intervals.select { |x, y| a < y && x < b }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def insert(a, b)
|
|
34
|
+
return false if a >= b || !intersect(a, b).empty?
|
|
35
|
+
|
|
36
|
+
@intervals << [a, b]
|
|
37
|
+
@intervals.sort!
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remove(a, b)
|
|
42
|
+
hit = intersect(a, b)
|
|
43
|
+
@intervals -= hit
|
|
44
|
+
hit.size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def size
|
|
48
|
+
@intervals.size
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class TestDisjointIntervalTree < Minitest::Test
|
|
53
|
+
def setup
|
|
54
|
+
@tree = DisjointIntervalTree.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def teardown
|
|
58
|
+
@tree.check!
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# a tree holding [0..10[ [20..30[ [40..50[ [60..70[
|
|
62
|
+
def spaced_tree
|
|
63
|
+
tree = DisjointIntervalTree.new
|
|
64
|
+
4.times { |i| tree.insert(20 * i, 20 * i + 10) }
|
|
65
|
+
tree
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##########
|
|
69
|
+
# BASICS #
|
|
70
|
+
##########
|
|
71
|
+
|
|
72
|
+
def test_empty_tree
|
|
73
|
+
assert_empty @tree
|
|
74
|
+
assert_equal 0, @tree.size
|
|
75
|
+
assert_equal 0, @tree.length
|
|
76
|
+
assert_equal 0, @tree.height
|
|
77
|
+
assert_equal [], @tree.to_a
|
|
78
|
+
assert_nil @tree.at(0)
|
|
79
|
+
assert_nil @tree.hull
|
|
80
|
+
refute @tree.cover?(0)
|
|
81
|
+
refute @tree.intersect?(0, 100)
|
|
82
|
+
assert_equal [], @tree.intersect(0, 100)
|
|
83
|
+
assert_equal 0, @tree.remove(0, 100)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_insert_returns_self
|
|
87
|
+
assert_same @tree, @tree.insert(0, 10)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_insert_single
|
|
91
|
+
@tree.insert(10, 20)
|
|
92
|
+
|
|
93
|
+
refute_empty @tree
|
|
94
|
+
assert_equal 1, @tree.size
|
|
95
|
+
assert_equal 1, @tree.height
|
|
96
|
+
assert_equal [[10, 20]], @tree.to_a
|
|
97
|
+
assert_equal [10, 20], @tree.hull
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_intervals_are_half_open
|
|
101
|
+
@tree.insert(10, 20)
|
|
102
|
+
|
|
103
|
+
assert_nil @tree.at(9)
|
|
104
|
+
assert_equal [10, 20], @tree.at(10)
|
|
105
|
+
assert_equal [10, 20], @tree.at(19)
|
|
106
|
+
assert_nil @tree.at(20)
|
|
107
|
+
|
|
108
|
+
refute @tree.cover?(9)
|
|
109
|
+
assert @tree.cover?(10)
|
|
110
|
+
assert @tree.cover?(19)
|
|
111
|
+
refute @tree.cover?(20)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_adjacent_intervals_do_not_overlap
|
|
115
|
+
@tree.insert(0, 10)
|
|
116
|
+
@tree.insert(10, 20)
|
|
117
|
+
@tree.insert(20, 30)
|
|
118
|
+
|
|
119
|
+
assert_equal 3, @tree.size
|
|
120
|
+
assert_equal [[0, 10], [10, 20], [20, 30]], @tree.to_a
|
|
121
|
+
assert_equal [0, 30], @tree.hull
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_intervals_are_ordered
|
|
125
|
+
[50, 10, 90, 30, 70, 0, 20].each { |a| @tree.insert(a, a + 5) }
|
|
126
|
+
|
|
127
|
+
assert_equal [[0, 5], [10, 15], [20, 25], [30, 35], [50, 55], [70, 75], [90, 95]],
|
|
128
|
+
@tree.to_a
|
|
129
|
+
assert_equal @tree.to_a, @tree.to_a.sort
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_coverage
|
|
133
|
+
@tree.insert(0, 10)
|
|
134
|
+
@tree.insert(100, 130)
|
|
135
|
+
|
|
136
|
+
assert_equal 40, @tree.coverage
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_inspect
|
|
140
|
+
@tree.insert(0, 10)
|
|
141
|
+
|
|
142
|
+
assert_equal '#<DisjointIntervalTree size=1 height=1>', @tree.inspect
|
|
143
|
+
assert_equal @tree.inspect, @tree.to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
############
|
|
147
|
+
# CONTRACT #
|
|
148
|
+
############
|
|
149
|
+
|
|
150
|
+
def test_insert_overlapping_raises
|
|
151
|
+
@tree.insert(10, 20)
|
|
152
|
+
@tree.insert(30, 40)
|
|
153
|
+
|
|
154
|
+
overlapping = [
|
|
155
|
+
[10, 20], # identical
|
|
156
|
+
[12, 18], # strictly included
|
|
157
|
+
[5, 25], # strictly including
|
|
158
|
+
[5, 11], # overlapping on the left
|
|
159
|
+
[19, 25], # overlapping on the right
|
|
160
|
+
[0, 35] # spanning both
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
overlapping.each do |a, b|
|
|
164
|
+
assert_raises(DisjointIntervalTree::OverlapError) { @tree.insert(a, b) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# the tree was left untouched
|
|
168
|
+
assert_equal [[10, 20], [30, 40]], @tree.to_a
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_overlap_error_is_a_standard_error
|
|
172
|
+
@tree.insert(10, 20)
|
|
173
|
+
|
|
174
|
+
error = assert_raises(DisjointIntervalTree::OverlapError) { @tree.insert(15, 25) }
|
|
175
|
+
|
|
176
|
+
assert_kind_of DisjointIntervalTree::Error, error
|
|
177
|
+
assert_kind_of StandardError, error
|
|
178
|
+
assert_match(/\[15\.\.25\[ overlaps \[10\.\.20\[/, error.message)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_insert_predicate_does_not_raise
|
|
182
|
+
assert @tree.insert?(10, 20)
|
|
183
|
+
refute @tree.insert?(15, 25)
|
|
184
|
+
assert @tree.insert?(20, 25)
|
|
185
|
+
assert_equal [[10, 20], [20, 25]], @tree.to_a
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_insert_empty_interval_raises
|
|
189
|
+
assert_raises(ArgumentError) { @tree.insert(10, 10) }
|
|
190
|
+
assert_raises(ArgumentError) { @tree.insert(20, 10) }
|
|
191
|
+
assert_raises(ArgumentError) { @tree.insert?(10, 10) }
|
|
192
|
+
assert_empty @tree
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_insert_invalid_bounds_raise
|
|
196
|
+
# negative bounds must not silently wrap around
|
|
197
|
+
assert_raises(RangeError) { @tree.insert(-1, 10) }
|
|
198
|
+
assert_raises(RangeError) { @tree.insert(-(2**63), 10) }
|
|
199
|
+
assert_raises(RangeError) { @tree.insert(-(2**70), 10) }
|
|
200
|
+
assert_raises(RangeError) { @tree.insert(0, -1) }
|
|
201
|
+
|
|
202
|
+
# nor must out of range ones
|
|
203
|
+
assert_raises(RangeError) { @tree.insert(0, DisjointIntervalTree::MAX + 1) }
|
|
204
|
+
assert_raises(RangeError) { @tree.insert(2**64, 2**65) }
|
|
205
|
+
|
|
206
|
+
# only integers are accepted, floats are not silently truncated
|
|
207
|
+
assert_raises(TypeError) { @tree.insert(1.5, 10.5) }
|
|
208
|
+
assert_raises(TypeError) { @tree.insert('0', 10) }
|
|
209
|
+
assert_raises(TypeError) { @tree.insert(nil, 10) }
|
|
210
|
+
|
|
211
|
+
assert_empty @tree
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_invalid_bounds_raise_everywhere
|
|
215
|
+
assert_raises(RangeError) { @tree.remove(-1, 10) }
|
|
216
|
+
assert_raises(RangeError) { @tree.intersect(-1, 10) }
|
|
217
|
+
assert_raises(RangeError) { @tree.intersect?(-1, 10) }
|
|
218
|
+
assert_raises(RangeError) { @tree.at(-1) }
|
|
219
|
+
assert_raises(RangeError) { @tree.cover?(-1) }
|
|
220
|
+
assert_raises(TypeError) { @tree.at('nope') }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_large_bounds_are_supported
|
|
224
|
+
@tree.insert(2**63, 2**63 + 10)
|
|
225
|
+
|
|
226
|
+
assert_equal [[2**63, 2**63 + 10]], @tree.to_a
|
|
227
|
+
assert_equal [2**63, 2**63 + 10], @tree.at(2**63 + 5)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
##############
|
|
231
|
+
# INTERSECTS #
|
|
232
|
+
##############
|
|
233
|
+
|
|
234
|
+
def test_intersect_yields_matching_intervals
|
|
235
|
+
tree = spaced_tree
|
|
236
|
+
seen = []
|
|
237
|
+
result = tree.intersect(5, 45) { |a, b| seen << [a, b] }
|
|
238
|
+
|
|
239
|
+
assert_same tree, result
|
|
240
|
+
assert_equal [[0, 10], [20, 30], [40, 50]], seen
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def test_intersect_without_block_returns_an_array
|
|
244
|
+
assert_equal [[0, 10], [20, 30], [40, 50]], spaced_tree.intersect(5, 45)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def test_intersect_reports_nothing_when_querying_a_hole
|
|
248
|
+
assert_equal [], spaced_tree.intersect(12, 18)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_intersect_ignores_adjacency
|
|
252
|
+
tree = spaced_tree
|
|
253
|
+
|
|
254
|
+
assert_equal [], tree.intersect(10, 20)
|
|
255
|
+
assert_equal [[0, 10]], tree.intersect(9, 20)
|
|
256
|
+
assert_equal [[20, 30]], tree.intersect(10, 21)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def test_intersect_on_an_empty_query
|
|
260
|
+
assert_equal [], spaced_tree.intersect(5, 5)
|
|
261
|
+
assert_equal [], spaced_tree.intersect(45, 5)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def test_intersect_out_of_range
|
|
265
|
+
assert_equal [], spaced_tree.intersect(1000, 2000)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_intersect_everything
|
|
269
|
+
assert_equal 4, spaced_tree.intersect(0, 1000).size
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_intersect_predicate
|
|
273
|
+
tree = spaced_tree
|
|
274
|
+
|
|
275
|
+
refute tree.intersect?(12, 18)
|
|
276
|
+
refute tree.intersect?(10, 20)
|
|
277
|
+
assert tree.intersect?(9, 20)
|
|
278
|
+
assert tree.intersect?(0, 1000)
|
|
279
|
+
assert tree.overlaps?(0, 1000)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_each_intersecting_is_an_alias
|
|
283
|
+
assert_equal spaced_tree.intersect(5, 45), spaced_tree.each_intersecting(5, 45)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def test_intersect_can_be_broken_out_of
|
|
287
|
+
tree = spaced_tree
|
|
288
|
+
seen = []
|
|
289
|
+
|
|
290
|
+
result = tree.intersect(0, 1000) do |a, b|
|
|
291
|
+
seen << [a, b]
|
|
292
|
+
break :stopped if seen.size == 2
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
assert_equal :stopped, result
|
|
296
|
+
assert_equal [[0, 10], [20, 30]], seen
|
|
297
|
+
|
|
298
|
+
# the traversal state was properly restored
|
|
299
|
+
tree.insert(100, 110)
|
|
300
|
+
assert_equal 5, tree.size
|
|
301
|
+
tree.check!
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def test_an_exception_raised_in_a_block_leaves_the_tree_usable
|
|
305
|
+
tree = spaced_tree
|
|
306
|
+
|
|
307
|
+
assert_raises(RuntimeError) do
|
|
308
|
+
tree.intersect(0, 1000) { raise 'boom' }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
tree.insert(100, 110)
|
|
312
|
+
assert_equal 5, tree.size
|
|
313
|
+
tree.check!
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
##############
|
|
317
|
+
# REENTRANCY #
|
|
318
|
+
##############
|
|
319
|
+
|
|
320
|
+
def test_mutating_while_traversing_raises
|
|
321
|
+
tree = spaced_tree
|
|
322
|
+
|
|
323
|
+
assert_raises(DisjointIntervalTree::Error) do
|
|
324
|
+
tree.each { tree.insert(1000, 1010) }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
assert_raises(DisjointIntervalTree::Error) do
|
|
328
|
+
tree.intersect(0, 1000) { tree.remove(0, 10) }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
assert_raises(DisjointIntervalTree::Error) do
|
|
332
|
+
tree.each { tree.clear }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# and the tree is unharmed
|
|
336
|
+
assert_equal 4, tree.size
|
|
337
|
+
tree.check!
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_mutating_a_frozen_tree_raises
|
|
341
|
+
tree = spaced_tree.freeze
|
|
342
|
+
|
|
343
|
+
assert_raises(FrozenError) { tree.insert(100, 110) }
|
|
344
|
+
assert_raises(FrozenError) { tree.insert?(100, 110) }
|
|
345
|
+
assert_raises(FrozenError) { tree.remove(0, 10) }
|
|
346
|
+
assert_raises(FrozenError) { tree.clear }
|
|
347
|
+
|
|
348
|
+
# reading is still fine
|
|
349
|
+
assert_equal 4, tree.size
|
|
350
|
+
assert_equal [[0, 10]], tree.intersect(0, 10)
|
|
351
|
+
tree.check!
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def test_reading_while_traversing_is_allowed
|
|
355
|
+
tree = spaced_tree
|
|
356
|
+
sizes = []
|
|
357
|
+
|
|
358
|
+
tree.each { sizes << tree.size + tree.intersect(0, 1000).size }
|
|
359
|
+
|
|
360
|
+
assert_equal [8, 8, 8, 8], sizes
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
##########
|
|
364
|
+
# REMOVE #
|
|
365
|
+
##########
|
|
366
|
+
|
|
367
|
+
def test_remove_returns_the_number_of_removed_intervals
|
|
368
|
+
tree = spaced_tree
|
|
369
|
+
|
|
370
|
+
assert_equal 0, tree.remove(10, 20)
|
|
371
|
+
assert_equal 4, tree.size
|
|
372
|
+
|
|
373
|
+
assert_equal 3, tree.remove(5, 45)
|
|
374
|
+
assert_equal [[60, 70]], tree.to_a
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def test_remove_does_not_split_intervals
|
|
378
|
+
tree = spaced_tree
|
|
379
|
+
|
|
380
|
+
assert_equal 1, tree.remove(25, 26)
|
|
381
|
+
assert_nil tree.at(20)
|
|
382
|
+
assert_nil tree.at(29)
|
|
383
|
+
assert_equal [[0, 10], [40, 50], [60, 70]], tree.to_a
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def test_remove_ignores_adjacency
|
|
387
|
+
tree = spaced_tree
|
|
388
|
+
|
|
389
|
+
assert_equal 0, tree.remove(10, 20)
|
|
390
|
+
assert_equal 0, tree.remove(30, 40)
|
|
391
|
+
assert_equal 4, tree.size
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def test_remove_on_an_empty_query
|
|
395
|
+
tree = spaced_tree
|
|
396
|
+
|
|
397
|
+
assert_equal 0, tree.remove(5, 5)
|
|
398
|
+
assert_equal 0, tree.remove(45, 5)
|
|
399
|
+
assert_equal 4, tree.size
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def test_remove_yields_removed_intervals
|
|
403
|
+
tree = spaced_tree
|
|
404
|
+
seen = []
|
|
405
|
+
|
|
406
|
+
assert_equal 3, tree.remove(5, 45) { |a, b| seen << [a, b] }
|
|
407
|
+
assert_equal [[0, 10], [20, 30], [40, 50]], seen
|
|
408
|
+
assert_equal [[60, 70]], tree.to_a
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def test_remove_then_reinsert
|
|
412
|
+
tree = spaced_tree
|
|
413
|
+
|
|
414
|
+
tree.remove(20, 30)
|
|
415
|
+
assert_raises(DisjointIntervalTree::OverlapError) { tree.insert(0, 10) }
|
|
416
|
+
tree.insert(20, 30)
|
|
417
|
+
assert_equal 4, tree.size
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def test_remove_everything
|
|
421
|
+
tree = spaced_tree
|
|
422
|
+
|
|
423
|
+
assert_equal 4, tree.remove(0, DisjointIntervalTree::MAX)
|
|
424
|
+
assert_empty tree
|
|
425
|
+
assert_equal 0, tree.remove(0, DisjointIntervalTree::MAX)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def test_clear
|
|
429
|
+
tree = spaced_tree
|
|
430
|
+
|
|
431
|
+
assert_same tree, tree.clear
|
|
432
|
+
assert_empty tree
|
|
433
|
+
assert_equal [], tree.to_a
|
|
434
|
+
|
|
435
|
+
tree.insert(0, 1000)
|
|
436
|
+
assert_equal 1, tree.size
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
##############
|
|
440
|
+
# ENUMERABLE #
|
|
441
|
+
##############
|
|
442
|
+
|
|
443
|
+
def test_each_returns_an_enumerator_without_a_block
|
|
444
|
+
tree = spaced_tree
|
|
445
|
+
enumerator = tree.each
|
|
446
|
+
|
|
447
|
+
assert_kind_of Enumerator, enumerator
|
|
448
|
+
assert_equal 4, enumerator.size
|
|
449
|
+
assert_equal [[0, 10], [20, 30], [40, 50], [60, 70]], enumerator.to_a
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def test_enumerable_methods
|
|
453
|
+
tree = spaced_tree
|
|
454
|
+
|
|
455
|
+
assert_equal [10, 10, 10, 10], tree.map { |a, b| b - a }
|
|
456
|
+
assert_equal [[0, 10]], tree.select { |a, _| a.zero? }
|
|
457
|
+
assert_equal [0, 10], tree.first
|
|
458
|
+
assert_equal [[0, 10], [20, 30]], tree.first(2)
|
|
459
|
+
assert tree.include?([20, 30])
|
|
460
|
+
refute tree.include?([20, 31])
|
|
461
|
+
assert_equal 4, tree.count
|
|
462
|
+
assert_equal 40, tree.sum { |a, b| b - a }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def test_entries_is_an_alias_of_to_a
|
|
466
|
+
assert_equal spaced_tree.to_a, spaced_tree.entries
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
###############
|
|
470
|
+
# CONSTRUCTOR #
|
|
471
|
+
###############
|
|
472
|
+
|
|
473
|
+
def test_new_with_intervals
|
|
474
|
+
tree = DisjointIntervalTree.new([[20, 30], [0, 10]])
|
|
475
|
+
|
|
476
|
+
assert_equal [[0, 10], [20, 30]], tree.to_a
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def test_new_with_overlapping_intervals_raises
|
|
480
|
+
assert_raises(DisjointIntervalTree::OverlapError) do
|
|
481
|
+
DisjointIntervalTree.new([[0, 10], [5, 15]])
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def test_new_with_garbage_raises
|
|
486
|
+
assert_raises(TypeError) { DisjointIntervalTree.new(42) }
|
|
487
|
+
assert_raises(ArgumentError) { DisjointIntervalTree.new([[0, 10, 20]]) }
|
|
488
|
+
assert_raises(ArgumentError) { DisjointIntervalTree.new([0]) }
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def test_dup_is_a_deep_copy
|
|
492
|
+
tree = spaced_tree
|
|
493
|
+
copy = tree.dup
|
|
494
|
+
|
|
495
|
+
assert_equal tree, copy
|
|
496
|
+
assert_equal tree.to_a, copy.to_a
|
|
497
|
+
|
|
498
|
+
copy.insert(100, 110)
|
|
499
|
+
refute_equal tree, copy
|
|
500
|
+
assert_equal 4, tree.size
|
|
501
|
+
assert_equal 5, copy.size
|
|
502
|
+
|
|
503
|
+
copy.check!
|
|
504
|
+
tree.check!
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def test_equality
|
|
508
|
+
assert_equal DisjointIntervalTree.new, DisjointIntervalTree.new
|
|
509
|
+
assert_equal spaced_tree, spaced_tree
|
|
510
|
+
refute_equal spaced_tree, DisjointIntervalTree.new
|
|
511
|
+
refute_equal spaced_tree, [[0, 10], [20, 30], [40, 50], [60, 70]]
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
##################
|
|
515
|
+
# EXTREME VALUES #
|
|
516
|
+
##################
|
|
517
|
+
|
|
518
|
+
def test_extreme_values
|
|
519
|
+
max = DisjointIntervalTree::MAX
|
|
520
|
+
|
|
521
|
+
@tree.insert(0, 1)
|
|
522
|
+
@tree.insert(max - 1, max)
|
|
523
|
+
@tree.insert(1, max - 1)
|
|
524
|
+
|
|
525
|
+
assert_equal [0, max], @tree.hull
|
|
526
|
+
assert_equal [max - 1, max], @tree.at(max - 1)
|
|
527
|
+
|
|
528
|
+
# no half-open interval can ever contain MAX
|
|
529
|
+
assert_nil @tree.at(max)
|
|
530
|
+
refute @tree.cover?(max)
|
|
531
|
+
|
|
532
|
+
assert_equal 3, @tree.intersect(0, max).size
|
|
533
|
+
assert_equal 3, @tree.remove(0, max)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
###########
|
|
537
|
+
# BALANCE #
|
|
538
|
+
###########
|
|
539
|
+
|
|
540
|
+
def test_sorted_insertions_stay_balanced
|
|
541
|
+
n = 4095
|
|
542
|
+
n.times { |i| @tree.insert(2 * i, 2 * i + 1) }
|
|
543
|
+
|
|
544
|
+
assert_equal n, @tree.size
|
|
545
|
+
|
|
546
|
+
# 4095 nodes fit in a perfectly balanced tree of height 12, an AVL tree is
|
|
547
|
+
# at most ~1.44*log2(n) deep
|
|
548
|
+
assert_operator @tree.height, :<=, 18
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def test_reverse_sorted_insertions_stay_balanced
|
|
552
|
+
n = 4095
|
|
553
|
+
n.downto(1) { |i| @tree.insert(2 * i, 2 * i + 1) }
|
|
554
|
+
|
|
555
|
+
assert_equal n, @tree.size
|
|
556
|
+
assert_operator @tree.height, :<=, 18
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def test_sequential_removals_stay_balanced
|
|
560
|
+
n = 1023
|
|
561
|
+
n.times { |i| @tree.insert(2 * i, 2 * i + 1) }
|
|
562
|
+
|
|
563
|
+
n.times do |i|
|
|
564
|
+
assert_equal 1, @tree.remove(2 * i, 2 * i + 1)
|
|
565
|
+
assert_equal n - i - 1, @tree.size
|
|
566
|
+
assert_operator @tree.height, :<=, 16
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
assert_empty @tree
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
##############
|
|
573
|
+
# RANDOMIZED #
|
|
574
|
+
##############
|
|
575
|
+
|
|
576
|
+
def test_random_against_a_reference_model
|
|
577
|
+
srand(Integer(ENV.fetch('SEED', 1)))
|
|
578
|
+
|
|
579
|
+
universe = 512
|
|
580
|
+
model = IntervalModel.new
|
|
581
|
+
|
|
582
|
+
2000.times do |iteration|
|
|
583
|
+
a = rand(universe)
|
|
584
|
+
b = a + 1 + rand(8)
|
|
585
|
+
|
|
586
|
+
case rand(3)
|
|
587
|
+
when 0
|
|
588
|
+
if model.intersect(a, b).empty?
|
|
589
|
+
@tree.insert(a, b)
|
|
590
|
+
model.insert(a, b)
|
|
591
|
+
else
|
|
592
|
+
assert_raises(DisjointIntervalTree::OverlapError) { @tree.insert(a, b) }
|
|
593
|
+
end
|
|
594
|
+
when 1
|
|
595
|
+
assert_equal model.remove(a, b), @tree.remove(a, b),
|
|
596
|
+
"iteration #{iteration}: remove(#{a}, #{b})"
|
|
597
|
+
else
|
|
598
|
+
assert_equal model.intersect(a, b), @tree.intersect(a, b),
|
|
599
|
+
"iteration #{iteration}: intersect(#{a}, #{b})"
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
assert_equal model.size, @tree.size, "iteration #{iteration}"
|
|
603
|
+
assert_equal model.intervals, @tree.to_a, "iteration #{iteration}"
|
|
604
|
+
@tree.check!
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
refute_empty @tree
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def test_random_point_queries
|
|
611
|
+
srand(Integer(ENV.fetch('SEED', 1)))
|
|
612
|
+
|
|
613
|
+
model = IntervalModel.new
|
|
614
|
+
200.times do
|
|
615
|
+
a = rand(4096)
|
|
616
|
+
b = a + 1 + rand(16)
|
|
617
|
+
model.insert(a, b) && @tree.insert(a, b)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
@tree.check!
|
|
621
|
+
|
|
622
|
+
2000.times do
|
|
623
|
+
x = rand(4096 + 32)
|
|
624
|
+
expected = model.intervals.find { |a, b| a <= x && x < b }
|
|
625
|
+
|
|
626
|
+
if expected
|
|
627
|
+
assert_equal expected, @tree.at(x), "at(#{x})"
|
|
628
|
+
assert @tree.cover?(x), "cover?(#{x})"
|
|
629
|
+
else
|
|
630
|
+
assert_nil @tree.at(x), "at(#{x})"
|
|
631
|
+
refute @tree.cover?(x), "cover?(#{x})"
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def test_garbage_collection
|
|
637
|
+
100.times do
|
|
638
|
+
tree = DisjointIntervalTree.new
|
|
639
|
+
100.times { |i| tree.insert(10 * i, 10 * i + 5) }
|
|
640
|
+
tree.check!
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
GC.start
|
|
644
|
+
GC.compact if GC.respond_to?(:compact)
|
|
645
|
+
|
|
646
|
+
@tree.insert(0, 10)
|
|
647
|
+
assert_equal [[0, 10]], @tree.to_a
|
|
648
|
+
end
|
|
649
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: disjoint_interval_tree
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Romain PEREIRA
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
A set of pairwise disjoint half-open intervals [a..b[, backed by an
|
|
15
|
+
augmented AVL tree written in C. Insertion, point lookup, intersection
|
|
16
|
+
queries and range removal are all logarithmic. Each node caches the hull of
|
|
17
|
+
the subtree it roots, so intersection queries prune whole subtrees in O(1).
|
|
18
|
+
email:
|
|
19
|
+
- romain.pereira@inria.fr
|
|
20
|
+
- rpereira@anl.gov
|
|
21
|
+
executables: []
|
|
22
|
+
extensions:
|
|
23
|
+
- ext/disjoint_interval_tree/extconf.rb
|
|
24
|
+
extra_rdoc_files: []
|
|
25
|
+
files:
|
|
26
|
+
- CITATION.cff
|
|
27
|
+
- README.md
|
|
28
|
+
- Rakefile
|
|
29
|
+
- disjoint_interval_tree.gemspec
|
|
30
|
+
- ext/disjoint_interval_tree/dit.c
|
|
31
|
+
- ext/disjoint_interval_tree/dit.h
|
|
32
|
+
- ext/disjoint_interval_tree/dit_ruby.c
|
|
33
|
+
- ext/disjoint_interval_tree/extconf.rb
|
|
34
|
+
- lib/disjoint_interval_tree.rb
|
|
35
|
+
- lib/disjoint_interval_tree/version.rb
|
|
36
|
+
- test/c/Makefile
|
|
37
|
+
- test/c/test_dit.c
|
|
38
|
+
- test/test_disjoint_interval_tree.rb
|
|
39
|
+
homepage: https://github.com/anlsys/ruby-disjoint-interval-tree
|
|
40
|
+
licenses:
|
|
41
|
+
- CECILL-C
|
|
42
|
+
metadata:
|
|
43
|
+
source_code_uri: https://github.com/anlsys/ruby-disjoint-interval-tree
|
|
44
|
+
bug_tracker_uri: https://github.com/anlsys/ruby-disjoint-interval-tree/issues
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 2.7.0
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.5.16
|
|
62
|
+
signing_key:
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Disjoint interval tree, in C, with Ruby bindings
|
|
65
|
+
test_files: []
|