disjoint_interval_tree 0.1.0 → 0.2.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.
@@ -27,13 +27,13 @@ class IntervalModel
27
27
  def intersect(a, b)
28
28
  return [] if a >= b
29
29
 
30
- @intervals.select { |x, y| a < y && x < b }
30
+ @intervals.select { |x, y, _| a < y && x < b }
31
31
  end
32
32
 
33
- def insert(a, b)
33
+ def insert(a, b, obj = nil)
34
34
  return false if a >= b || !intersect(a, b).empty?
35
35
 
36
- @intervals << [a, b]
36
+ @intervals << [a, b, obj]
37
37
  @intervals.sort!
38
38
  true
39
39
  end
@@ -93,7 +93,7 @@ class TestDisjointIntervalTree < Minitest::Test
93
93
  refute_empty @tree
94
94
  assert_equal 1, @tree.size
95
95
  assert_equal 1, @tree.height
96
- assert_equal [[10, 20]], @tree.to_a
96
+ assert_equal [[10, 20, nil]], @tree.to_a
97
97
  assert_equal [10, 20], @tree.hull
98
98
  end
99
99
 
@@ -101,8 +101,8 @@ class TestDisjointIntervalTree < Minitest::Test
101
101
  @tree.insert(10, 20)
102
102
 
103
103
  assert_nil @tree.at(9)
104
- assert_equal [10, 20], @tree.at(10)
105
- assert_equal [10, 20], @tree.at(19)
104
+ assert_equal [10, 20, nil], @tree.at(10)
105
+ assert_equal [10, 20, nil], @tree.at(19)
106
106
  assert_nil @tree.at(20)
107
107
 
108
108
  refute @tree.cover?(9)
@@ -117,14 +117,15 @@ class TestDisjointIntervalTree < Minitest::Test
117
117
  @tree.insert(20, 30)
118
118
 
119
119
  assert_equal 3, @tree.size
120
- assert_equal [[0, 10], [10, 20], [20, 30]], @tree.to_a
120
+ assert_equal [[0, 10, nil], [10, 20, nil], [20, 30, nil]], @tree.to_a
121
121
  assert_equal [0, 30], @tree.hull
122
122
  end
123
123
 
124
124
  def test_intervals_are_ordered
125
125
  [50, 10, 90, 30, 70, 0, 20].each { |a| @tree.insert(a, a + 5) }
126
126
 
127
- assert_equal [[0, 5], [10, 15], [20, 25], [30, 35], [50, 55], [70, 75], [90, 95]],
127
+ assert_equal [[0, 5, nil], [10, 15, nil], [20, 25, nil], [30, 35, nil],
128
+ [50, 55, nil], [70, 75, nil], [90, 95, nil]],
128
129
  @tree.to_a
129
130
  assert_equal @tree.to_a, @tree.to_a.sort
130
131
  end
@@ -165,7 +166,7 @@ class TestDisjointIntervalTree < Minitest::Test
165
166
  end
166
167
 
167
168
  # the tree was left untouched
168
- assert_equal [[10, 20], [30, 40]], @tree.to_a
169
+ assert_equal [[10, 20, nil], [30, 40, nil]], @tree.to_a
169
170
  end
170
171
 
171
172
  def test_overlap_error_is_a_standard_error
@@ -182,7 +183,7 @@ class TestDisjointIntervalTree < Minitest::Test
182
183
  assert @tree.insert?(10, 20)
183
184
  refute @tree.insert?(15, 25)
184
185
  assert @tree.insert?(20, 25)
185
- assert_equal [[10, 20], [20, 25]], @tree.to_a
186
+ assert_equal [[10, 20, nil], [20, 25, nil]], @tree.to_a
186
187
  end
187
188
 
188
189
  def test_insert_empty_interval_raises
@@ -223,8 +224,8 @@ class TestDisjointIntervalTree < Minitest::Test
223
224
  def test_large_bounds_are_supported
224
225
  @tree.insert(2**63, 2**63 + 10)
225
226
 
226
- assert_equal [[2**63, 2**63 + 10]], @tree.to_a
227
- assert_equal [2**63, 2**63 + 10], @tree.at(2**63 + 5)
227
+ assert_equal [[2**63, 2**63 + 10, nil]], @tree.to_a
228
+ assert_equal [2**63, 2**63 + 10, nil], @tree.at(2**63 + 5)
228
229
  end
229
230
 
230
231
  ##############
@@ -234,14 +235,14 @@ class TestDisjointIntervalTree < Minitest::Test
234
235
  def test_intersect_yields_matching_intervals
235
236
  tree = spaced_tree
236
237
  seen = []
237
- result = tree.intersect(5, 45) { |a, b| seen << [a, b] }
238
+ result = tree.intersect(5, 45) { |a, b, obj| seen << [a, b, obj] }
238
239
 
239
240
  assert_same tree, result
240
- assert_equal [[0, 10], [20, 30], [40, 50]], seen
241
+ assert_equal [[0, 10, nil], [20, 30, nil], [40, 50, nil]], seen
241
242
  end
242
243
 
243
244
  def test_intersect_without_block_returns_an_array
244
- assert_equal [[0, 10], [20, 30], [40, 50]], spaced_tree.intersect(5, 45)
245
+ assert_equal [[0, 10, nil], [20, 30, nil], [40, 50, nil]], spaced_tree.intersect(5, 45)
245
246
  end
246
247
 
247
248
  def test_intersect_reports_nothing_when_querying_a_hole
@@ -252,8 +253,8 @@ class TestDisjointIntervalTree < Minitest::Test
252
253
  tree = spaced_tree
253
254
 
254
255
  assert_equal [], tree.intersect(10, 20)
255
- assert_equal [[0, 10]], tree.intersect(9, 20)
256
- assert_equal [[20, 30]], tree.intersect(10, 21)
256
+ assert_equal [[0, 10, nil]], tree.intersect(9, 20)
257
+ assert_equal [[20, 30, nil]], tree.intersect(10, 21)
257
258
  end
258
259
 
259
260
  def test_intersect_on_an_empty_query
@@ -347,7 +348,7 @@ class TestDisjointIntervalTree < Minitest::Test
347
348
 
348
349
  # reading is still fine
349
350
  assert_equal 4, tree.size
350
- assert_equal [[0, 10]], tree.intersect(0, 10)
351
+ assert_equal [[0, 10, nil]], tree.intersect(0, 10)
351
352
  tree.check!
352
353
  end
353
354
 
@@ -371,7 +372,7 @@ class TestDisjointIntervalTree < Minitest::Test
371
372
  assert_equal 4, tree.size
372
373
 
373
374
  assert_equal 3, tree.remove(5, 45)
374
- assert_equal [[60, 70]], tree.to_a
375
+ assert_equal [[60, 70, nil]], tree.to_a
375
376
  end
376
377
 
377
378
  def test_remove_does_not_split_intervals
@@ -380,7 +381,7 @@ class TestDisjointIntervalTree < Minitest::Test
380
381
  assert_equal 1, tree.remove(25, 26)
381
382
  assert_nil tree.at(20)
382
383
  assert_nil tree.at(29)
383
- assert_equal [[0, 10], [40, 50], [60, 70]], tree.to_a
384
+ assert_equal [[0, 10, nil], [40, 50, nil], [60, 70, nil]], tree.to_a
384
385
  end
385
386
 
386
387
  def test_remove_ignores_adjacency
@@ -403,9 +404,9 @@ class TestDisjointIntervalTree < Minitest::Test
403
404
  tree = spaced_tree
404
405
  seen = []
405
406
 
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
407
+ assert_equal 3, tree.remove(5, 45) { |a, b, obj| seen << [a, b, obj] }
408
+ assert_equal [[0, 10, nil], [20, 30, nil], [40, 50, nil]], seen
409
+ assert_equal [[60, 70, nil]], tree.to_a
409
410
  end
410
411
 
411
412
  def test_remove_then_reinsert
@@ -446,18 +447,18 @@ class TestDisjointIntervalTree < Minitest::Test
446
447
 
447
448
  assert_kind_of Enumerator, enumerator
448
449
  assert_equal 4, enumerator.size
449
- assert_equal [[0, 10], [20, 30], [40, 50], [60, 70]], enumerator.to_a
450
+ assert_equal [[0, 10, nil], [20, 30, nil], [40, 50, nil], [60, 70, nil]], enumerator.to_a
450
451
  end
451
452
 
452
453
  def test_enumerable_methods
453
454
  tree = spaced_tree
454
455
 
455
456
  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])
457
+ assert_equal [[0, 10, nil]], tree.select { |a, _| a.zero? }
458
+ assert_equal [0, 10, nil], tree.first
459
+ assert_equal [[0, 10, nil], [20, 30, nil]], tree.first(2)
460
+ assert tree.include?([20, 30, nil])
461
+ refute tree.include?([20, 31, nil])
461
462
  assert_equal 4, tree.count
462
463
  assert_equal 40, tree.sum { |a, b| b - a }
463
464
  end
@@ -473,7 +474,7 @@ class TestDisjointIntervalTree < Minitest::Test
473
474
  def test_new_with_intervals
474
475
  tree = DisjointIntervalTree.new([[20, 30], [0, 10]])
475
476
 
476
- assert_equal [[0, 10], [20, 30]], tree.to_a
477
+ assert_equal [[0, 10, nil], [20, 30, nil]], tree.to_a
477
478
  end
478
479
 
479
480
  def test_new_with_overlapping_intervals_raises
@@ -484,7 +485,7 @@ class TestDisjointIntervalTree < Minitest::Test
484
485
 
485
486
  def test_new_with_garbage_raises
486
487
  assert_raises(TypeError) { DisjointIntervalTree.new(42) }
487
- assert_raises(ArgumentError) { DisjointIntervalTree.new([[0, 10, 20]]) }
488
+ assert_raises(ArgumentError) { DisjointIntervalTree.new([[0, 10, nil, 20]]) }
488
489
  assert_raises(ArgumentError) { DisjointIntervalTree.new([0]) }
489
490
  end
490
491
 
@@ -508,7 +509,7 @@ class TestDisjointIntervalTree < Minitest::Test
508
509
  assert_equal DisjointIntervalTree.new, DisjointIntervalTree.new
509
510
  assert_equal spaced_tree, spaced_tree
510
511
  refute_equal spaced_tree, DisjointIntervalTree.new
511
- refute_equal spaced_tree, [[0, 10], [20, 30], [40, 50], [60, 70]]
512
+ refute_equal spaced_tree, [[0, 10, nil], [20, 30, nil], [40, 50, nil], [60, 70, nil]]
512
513
  end
513
514
 
514
515
  ##################
@@ -523,7 +524,7 @@ class TestDisjointIntervalTree < Minitest::Test
523
524
  @tree.insert(1, max - 1)
524
525
 
525
526
  assert_equal [0, max], @tree.hull
526
- assert_equal [max - 1, max], @tree.at(max - 1)
527
+ assert_equal [max - 1, max, nil], @tree.at(max - 1)
527
528
 
528
529
  # no half-open interval can ever contain MAX
529
530
  assert_nil @tree.at(max)
@@ -586,8 +587,10 @@ class TestDisjointIntervalTree < Minitest::Test
586
587
  case rand(3)
587
588
  when 0
588
589
  if model.intersect(a, b).empty?
589
- @tree.insert(a, b)
590
- model.insert(a, b)
590
+ # an object whose content identifies the interval it belongs to, so
591
+ # that a payload moved to the wrong node is caught by the comparison
592
+ @tree.insert(a, b, "obj-#{a}")
593
+ model.insert(a, b, "obj-#{a}")
591
594
  else
592
595
  assert_raises(DisjointIntervalTree::OverlapError) { @tree.insert(a, b) }
593
596
  end
@@ -644,6 +647,192 @@ class TestDisjointIntervalTree < Minitest::Test
644
647
  GC.compact if GC.respond_to?(:compact)
645
648
 
646
649
  @tree.insert(0, 10)
647
- assert_equal [[0, 10]], @tree.to_a
650
+ assert_equal [[0, 10, nil]], @tree.to_a
651
+ end
652
+
653
+ ###########
654
+ # OBJECTS #
655
+ ###########
656
+
657
+ def test_objects_are_optional
658
+ @tree.insert(0, 10)
659
+ @tree.insert(20, 30, :something)
660
+
661
+ assert_equal [[0, 10, nil], [20, 30, :something]], @tree.to_a
662
+ assert_nil @tree[5]
663
+ assert_equal :something, @tree[25]
664
+ end
665
+
666
+ def test_objects_are_returned_by_identity
667
+ payload = Object.new
668
+ @tree.insert(0, 10, payload)
669
+
670
+ assert_same payload, @tree[5]
671
+ assert_same payload, @tree.at(5)[2]
672
+ assert_same payload, @tree.to_a.first[2]
673
+ assert_same payload, @tree.intersect(0, 100).first[2]
674
+
675
+ @tree.intersect(0, 100) { |_a, _b, obj| assert_same payload, obj }
676
+ @tree.each { |_a, _b, obj| assert_same payload, obj }
677
+ @tree.remove(0, 100) { |_a, _b, obj| assert_same payload, obj }
678
+ end
679
+
680
+ def test_any_object_can_be_stored
681
+ objects = [nil, false, true, 42, :sym, 'str', [1, 2], { a: 1 }, Object.new, 2**80]
682
+
683
+ objects.each_with_index { |obj, i| @tree.insert(10 * i, 10 * i + 5, obj) }
684
+ @tree.check!
685
+
686
+ objects.each_with_index do |obj, i|
687
+ assert_same obj, @tree[10 * i], "object #{obj.inspect}"
688
+ end
689
+ end
690
+
691
+ def test_several_intervals_may_share_an_object
692
+ shared = 'shared'
693
+ @tree.insert(0, 10, shared)
694
+ @tree.insert(20, 30, shared)
695
+
696
+ assert_same shared, @tree[5]
697
+ assert_same shared, @tree[25]
698
+
699
+ # removing one leaves the other untouched
700
+ @tree.remove(0, 10)
701
+ assert_same shared, @tree[25]
702
+ end
703
+
704
+ def test_aref_cannot_tell_a_nil_object_from_a_missing_interval
705
+ @tree.insert(0, 10, nil)
706
+
707
+ assert_nil @tree[5]
708
+ assert_nil @tree[15]
709
+
710
+ # ... but #at and #cover? can
711
+ assert_equal [0, 10, nil], @tree.at(5)
712
+ assert @tree.cover?(5)
713
+ assert_nil @tree.at(15)
714
+ refute @tree.cover?(15)
715
+ end
716
+
717
+ def test_objects_follow_their_interval_through_rebalancing
718
+ n = 512
719
+
720
+ # insert in an order that forces rotations, then delete in an order that
721
+ # forces two-children deletions -- the case that moves an interval from
722
+ # one node to another, and where its object could be left behind
723
+ (0...n).to_a.shuffle.each { |i| @tree.insert(10 * i, 10 * i + 5, "obj-#{i}") }
724
+ @tree.check!
725
+
726
+ @tree.each { |a, _b, obj| assert_equal "obj-#{a / 10}", obj }
727
+
728
+ (0...n).to_a.shuffle.each do |i|
729
+ assert_equal 1, @tree.remove(10 * i, 10 * i + 5) { |a, _b, obj|
730
+ assert_equal "obj-#{a / 10}", obj
731
+ }
732
+ @tree.each { |a, _b, obj| assert_equal "obj-#{a / 10}", obj }
733
+ end
734
+
735
+ assert_empty @tree
736
+ end
737
+
738
+ def test_dup_shares_the_objects
739
+ payload = Object.new
740
+ @tree.insert(0, 10, payload)
741
+
742
+ copy = @tree.dup
743
+
744
+ assert_equal @tree, copy
745
+ assert_same payload, copy[5]
746
+ assert_same @tree[5], copy[5]
747
+ end
748
+
749
+ def test_equality_compares_objects
750
+ one = DisjointIntervalTree.new([[0, 10, :a]])
751
+ two = DisjointIntervalTree.new([[0, 10, :a]])
752
+ three = DisjointIntervalTree.new([[0, 10, :b]])
753
+
754
+ assert_equal one, two
755
+ refute_equal one, three
756
+ end
757
+
758
+ def test_an_object_only_referenced_by_the_tree_survives_gc
759
+ @tree.insert(0, 10, +'only-the-tree-holds-me')
760
+ @tree.insert(20, 30, Object.new)
761
+
762
+ GC.start
763
+ GC.start
764
+
765
+ assert_equal 'only-the-tree-holds-me', @tree[5]
766
+ refute_nil @tree[25]
767
+ @tree.check!
768
+ end
769
+
770
+ # A finalizer must be built outside the scope holding the object it watches:
771
+ # its binding is kept alive by the ObjectSpace finalizer table, so a
772
+ # finalizer defined next to the object would keep that object reachable and
773
+ # the test would measure nothing.
774
+ def counting_finalizer(counter)
775
+ ->(_id) { counter[0] += 1 }
776
+ end
777
+
778
+ def fill_with_finalized_objects(tree, finalizer, n)
779
+ n.times do |i|
780
+ obj = Object.new
781
+ ObjectSpace.define_finalizer(obj, finalizer)
782
+ tree.insert(10 * i, 10 * i + 5, obj)
783
+ end
784
+ end
785
+
786
+ def test_objects_are_collectable_once_removed
787
+ counter = [0]
788
+ fill_with_finalized_objects(@tree, counting_finalizer(counter), 10)
789
+
790
+ GC.start
791
+ GC.start
792
+ assert_equal 0, counter[0], 'objects held by the tree must not be collected'
793
+
794
+ @tree.clear
795
+ GC.start
796
+ GC.start
797
+
798
+ assert_operator counter[0], :>, 0,
799
+ 'objects must become collectable once the tree drops them'
800
+ end
801
+
802
+ def test_objects_are_collectable_once_their_interval_is_removed
803
+ counter = [0]
804
+ fill_with_finalized_objects(@tree, counting_finalizer(counter), 10)
805
+
806
+ GC.start
807
+ assert_equal 0, counter[0]
808
+
809
+ assert_equal 4, @tree.remove(0, 35)
810
+ GC.start
811
+ GC.start
812
+
813
+ assert_operator counter[0], :>, 0
814
+ assert_equal 6, @tree.size
815
+ @tree.check!
816
+ end
817
+
818
+ def test_objects_survive_compaction
819
+ skip 'GC.compact is not available' unless GC.respond_to?(:compact)
820
+
821
+ 100.times { |i| @tree.insert(10 * i, 10 * i + 5, +"obj-#{i}") }
822
+
823
+ GC.compact
824
+
825
+ 100.times { |i| assert_equal "obj-#{i}", @tree[10 * i] }
826
+ @tree.check!
827
+ end
828
+
829
+ def test_objects_under_gc_stress
830
+ GC.stress = true
831
+
832
+ 20.times { |i| @tree.insert(10 * i, 10 * i + 5, +"obj-#{i}") }
833
+ assert_equal 20, @tree.intersect(0, 1000).size
834
+ @tree.each { |a, _b, obj| assert_equal "obj-#{a / 10}", obj }
835
+ ensure
836
+ GC.stress = false
648
837
  end
649
838
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: disjoint_interval_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Romain PEREIRA