rbtree-ruby 0.3.5 → 0.4.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.
data/lib/rbtree.rb CHANGED
@@ -54,6 +54,29 @@ require_relative "rbtree/version"
54
54
  class RBTree
55
55
  include Enumerable
56
56
 
57
+ # Key classes where <tt>(a <=> b) == 0</tt> and <tt>a.eql?(b)</tt> agree for any two
58
+ # instances of that same class, letting {#find_node} skip its fallback descent.
59
+ COHERENT_KEY_CLASSES = [
60
+ Integer, Float, Rational, String, Symbol, Time,
61
+ ].to_h { |klass| [klass, true] }
62
+ private_constant :COHERENT_KEY_CLASSES
63
+
64
+ # Declares that <tt>(a <=> b) == 0</tt> and +a.eql?(b)+ agree for instances of the
65
+ # given key class, speeding up lookups that miss the internal hash index.
66
+ #
67
+ # Optimization hint only: undeclared classes are still handled correctly, at
68
+ # O(log n) per missed lookup. Declaring a class that does not satisfy this makes
69
+ # such lookups report a present key as absent.
70
+ #
71
+ # @param klass [Class] the key class to declare coherent
72
+ # @return [Class] the declared class
73
+ # @example
74
+ # RBTree.coherent_key_class(Version)
75
+ def self.coherent_key_class(klass)
76
+ COHERENT_KEY_CLASSES[klass] = true
77
+ klass
78
+ end
79
+
57
80
  # Returns the number of key-value pairs stored in the tree.
58
81
  # @return [Integer] the number of entries in the tree
59
82
  attr_reader :key_count
@@ -100,9 +123,12 @@ class RBTree
100
123
  @root = @nil_node
101
124
  @min_node = @nil_node
102
125
  @max_node = @nil_node
103
- @hash_index = {} # Hash index for O(1) key lookup
126
+ @hash_index = {} # Hash index for O(1) key lookup, one entry per node
104
127
  @node_allocator = node_allocator
105
128
  @key_count = 0
129
+ @mod_count = 0 # bumped by changes a traversal can observe
130
+ @key_class = nil # single class of all keys, or false once mixed
131
+ @coherent_keys = false
106
132
 
107
133
  @overwrite = overwrite
108
134
 
@@ -111,10 +137,28 @@ class RBTree
111
137
  end
112
138
  end
113
139
 
114
- # Returns a Hash containing all key-value pairs from the tree.
140
+ # Creates a deep copy of the tree.
141
+ # Called automatically by `dup` and `clone`.
142
+ #
143
+ # The copy shares the original's node allocator instance.
144
+ #
145
+ # @param orig [RBTree] the original tree to copy
146
+ # @return [void]
147
+ def initialize_copy(orig)
148
+ initialize(
149
+ overwrite: orig.instance_variable_get(:@overwrite),
150
+ node_allocator: orig.instance_variable_get(:@node_allocator))
151
+ orig.each { |k, v| insert(k, v) }
152
+ end
153
+
154
+ # Returns a Hash containing all key-value pairs from the tree, in ascending key order.
115
155
  #
116
156
  # @return [Hash] a new Hash with the tree's contents
117
- def to_h = @hash_index.transform_values(&:value)
157
+ def to_h
158
+ h = {}
159
+ each { |k, v| h[k] = v }
160
+ h
161
+ end
118
162
 
119
163
  # Checks if the tree is empty.
120
164
  #
@@ -136,11 +180,19 @@ class RBTree
136
180
 
137
181
  # Returns the minimum key-value pair without removing it.
138
182
  #
139
- # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
183
+ # With no argument and no block this is an O(1) cached lookup; given a count or a
184
+ # comparison block, `Enumerable#min` semantics apply instead.
185
+ #
186
+ # @param args [Integer] optional number of smallest pairs to return
187
+ # @return [Array, nil] the pair, or the n smallest pairs, or nil if tree is empty
140
188
  # @example
141
189
  # tree = RBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
142
- # tree.min # => [1, "one"]
143
- def min = min_node&.pair
190
+ # tree.min # => [1, "one"]
191
+ # tree.min(2) # => [[1, "one"], [2, "two"]]
192
+ def min(*args, &block)
193
+ return super if !args.empty? || block
194
+ min_node&.pair
195
+ end
144
196
 
145
197
  # Returns the maximum key without removing it.
146
198
  #
@@ -152,37 +204,59 @@ class RBTree
152
204
 
153
205
  # Returns the maximum key-value pair without removing it.
154
206
  #
155
- # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
207
+ # With no argument and no block this is an O(1) cached lookup; given a count or a
208
+ # comparison block, `Enumerable#max` semantics apply instead.
209
+ #
210
+ # @param args [Integer] optional number of largest pairs to return
211
+ # @return [Array, nil] the pair, or the n largest pairs, or nil if tree is empty
156
212
  # @example
157
213
  # tree = RBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
158
- # tree.max # => [3, "three"]
159
- def max = max_node&.pair
214
+ # tree.max # => [3, "three"]
215
+ # tree.max(2) # => [[3, "three"], [2, "two"]]
216
+ def max(*args, &block)
217
+ return super if !args.empty? || block
218
+ max_node&.pair
219
+ end
160
220
 
161
- # Returns the first key-value pair without removing it.
221
+ # Returns the first key-value pair, or the first +n+ pairs, without removing them.
162
222
  #
163
- # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
223
+ # @param n [Integer, nil] number of leading pairs to return
224
+ # @return [Array, nil] the pair, or the n smallest pairs in ascending order
164
225
  # @example
165
226
  # tree = RBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
166
- # tree.first # => [1, "one"]
167
- def first = min
227
+ # tree.first # => [1, "one"]
228
+ # tree.first(2) # => [[1, "one"], [2, "two"]]
229
+ def first(n = nil) = n.nil? ? min : take(n)
168
230
 
169
- # Returns the last key-value pair without removing it.
231
+ # Returns the last key-value pair, or the last +n+ pairs, without removing them.
170
232
  #
171
- # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
233
+ # @param n [Integer, nil] number of trailing pairs to return
234
+ # @return [Array, nil] the pair, or the n largest pairs in ascending order
172
235
  # @example
173
236
  # tree = RBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
174
- # tree.last # => [3, "three"]
175
- def last = max
237
+ # tree.last # => [3, "three"]
238
+ # tree.last(2) # => [[2, "two"], [3, "three"]]
239
+ def last(n = nil)
240
+ return max if n.nil?
241
+ result = []
242
+ reverse_each { |pair| break if result.size >= n; result << pair }
243
+ result.reverse!
244
+ result
245
+ end
176
246
 
177
247
  # Checks if the tree contains the given key.
178
248
  #
249
+ # A key counts as present when <tt>(key <=> stored_key) == 0</tt>, even if the two
250
+ # objects are not +eql?+ (e.g. +1.0+ finds a stored +1+).
251
+ #
179
252
  # @param key [Object] the key to search for
180
253
  # @return [Boolean] true if the key exists in the tree, false otherwise
181
254
  # @example
182
255
  # tree = RBTree.new({1 => 'one', 2 => 'two'})
183
- # tree.key?(1) # => true
184
- # tree.key?(3) # => false
185
- def has_key?(key) = @hash_index.key?(key)
256
+ # tree.key?(1) # => true
257
+ # tree.key?(1.0) # => true
258
+ # tree.key?(3) # => false
259
+ def has_key?(key) = @hash_index.key?(key) || !find_node_by_order(key).nil?
186
260
  alias :key? :has_key?
187
261
 
188
262
  # Retrieves the value associated with the given key.
@@ -192,7 +266,7 @@ class RBTree
192
266
  # @example
193
267
  # tree = RBTree.new({1 => 'one', 2 => 'two'})
194
268
  # tree.get(1) # => "one"
195
- def value(key) = @hash_index[key]&.value
269
+ def value(key) = (@hash_index[key] || find_node_by_order(key))&.value
196
270
  alias :get :value
197
271
 
198
272
  # Retrieves a value associated with the given key, or a range of entries if a Range is provided.
@@ -353,6 +427,9 @@ class RBTree
353
427
  raise ArgumentError, "Source must be iterable"
354
428
  end
355
429
 
430
+ # Self-insertion (e.g. tree.merge!(tree)) must not mutate what it iterates.
431
+ source = source.to_a if source.equal?(self)
432
+
356
433
  source.each do |*pair|
357
434
  key, value = nil, nil
358
435
  if pair.size == 1 && pair[0].is_a?(Array)
@@ -369,21 +446,54 @@ class RBTree
369
446
  end
370
447
  alias :[]= :insert
371
448
 
449
+ # Returns a new tree containing the merged contents of self and other.
450
+ #
451
+ # When a block is given, it is called with (key, old_value, new_value) for
452
+ # duplicate keys, and the block's return value is used.
453
+ #
454
+ # @param other [RBTree, Hash, Enumerable] the source to merge from
455
+ # @yield [key, old_value, new_value] called for duplicate keys when block given
456
+ # @return [RBTree] a new tree with merged contents
457
+ def merge(other, &block)
458
+ dup.merge!(other, &block)
459
+ end
460
+
372
461
  # Merges the contents of another tree, hash, or enumerable into this tree.
373
462
  #
463
+ # When a block is given, it is called with (key, old_value, new_value) for
464
+ # duplicate keys, and the block's return value is used.
465
+ #
374
466
  # @param other [RBTree, Hash, Enumerable] the source to merge from
375
- # @param overwrite [Boolean] whether to overwrite existing keys (default: true)
467
+ # @param overwrite [Boolean] whether to overwrite existing keys (default: true). Ignored if block given.
468
+ # @yield [key, old_value, new_value] called for duplicate keys when block given
376
469
  # @return [RBTree] self
377
- def merge!(other, overwrite: true)
470
+ def merge!(other, overwrite: true, &block)
378
471
  if defined?(MultiRBTree) && other.is_a?(MultiRBTree)
379
472
  raise ArgumentError, "Cannot merge MultiRBTree into RBTree"
380
473
  end
381
- insert(other, overwrite: overwrite)
474
+ if block
475
+ other_enum = other.is_a?(Hash) || other.is_a?(RBTree) ? other : other.each
476
+ # Snapshot on self-merge; see RBTree#insert.
477
+ other_enum = other_enum.to_a if other_enum.equal?(self)
478
+ other_enum.each do |k, v|
479
+ if has_key?(k)
480
+ insert_entry(k, block.call(k, value(k), v), overwrite: true)
481
+ else
482
+ insert_entry(k, v)
483
+ end
484
+ end
485
+ else
486
+ insert(other, overwrite: overwrite)
487
+ end
382
488
  self
383
489
  end
384
490
 
385
491
  # Deletes the key-value pair with the specified key.
386
492
  #
493
+ # Entries whose value is +nil+ or +false+ are deleted correctly, so the return
494
+ # value alone cannot distinguish them from a missing key; use {#has_key?} first
495
+ # if that matters.
496
+ #
387
497
  # @param key [Object] the key to delete
388
498
  # @return [Object, nil] the value associated with the deleted key, or nil if not found
389
499
  # @example
@@ -391,8 +501,9 @@ class RBTree
391
501
  # tree.delete(1) # => "one"
392
502
  # tree.delete(3) # => nil
393
503
  def delete_key(key)
394
- return nil unless (value = (z = @hash_index[key])&.value)
395
- delete_indexed_node(key)
504
+ return nil unless (z = find_node(key))
505
+ value = z.value
506
+ delete_found_node(z)
396
507
  value
397
508
  end
398
509
  alias :delete :delete_key
@@ -427,11 +538,18 @@ class RBTree
427
538
 
428
539
  # Removes all key-value pairs from the tree.
429
540
  #
541
+ # Runs in O(1): the nodes are left to the garbage collector, and the allocator is
542
+ # told of the bulk discard so that pool statistics stay accurate.
543
+ #
430
544
  # @return [RBTree] self
431
545
  def clear
546
+ @node_allocator.discard(@key_count) if @key_count > 0
432
547
  @root = @min_node = @max_node = @nil_node
433
548
  @hash_index.clear
434
549
  @key_count = 0
550
+ @key_class = nil
551
+ @coherent_keys = false
552
+ @mod_count += 1
435
553
  self
436
554
  end
437
555
 
@@ -457,7 +575,8 @@ class RBTree
457
575
  # tree.delete(k) if k.even?
458
576
  # end
459
577
  def keys(reverse: false, safe: false, &block)
460
- return enum_for(__method__, reverse: reverse, safe: safe) { @key_count } unless block_given?
578
+ # `size`, not `key_count`: MultiRBTree yields a key once per value it holds.
579
+ return enum_for(__method__, reverse: reverse, safe: safe) { size } unless block_given?
461
580
  each(reverse: reverse, safe: safe) { |key, _| yield key }
462
581
  self
463
582
  end
@@ -602,6 +721,72 @@ class RBTree
602
721
  self
603
722
  end
604
723
 
724
+ # Returns a new tree containing key-value pairs for which the block returns true.
725
+ #
726
+ # @yield [key, value] each key-value pair
727
+ # @return [RBTree, Enumerator] a new tree with selected pairs, or Enumerator if no block
728
+ def select(&block)
729
+ return enum_for(__method__) { size } unless block_given?
730
+ result = new_derived_tree
731
+ each { |k, v| result.insert(k, v) if block.call(k, v) }
732
+ result
733
+ end
734
+
735
+ # Returns a new tree containing key-value pairs for which the block returns false.
736
+ #
737
+ # @yield [key, value] each key-value pair
738
+ # @return [RBTree, Enumerator] a new tree with non-rejected pairs, or Enumerator if no block
739
+ def reject(&block)
740
+ return enum_for(__method__) { size } unless block_given?
741
+ result = new_derived_tree
742
+ each { |k, v| result.insert(k, v) unless block.call(k, v) }
743
+ result
744
+ end
745
+
746
+ # Deletes key-value pairs for which the block returns true. Returns nil if no changes were made.
747
+ #
748
+ # @yield [key, value] each key-value pair
749
+ # @return [RBTree, nil, Enumerator] self if changed, nil if unchanged, or Enumerator if no block
750
+ def reject!(&block)
751
+ return enum_for(__method__) { size } unless block_given?
752
+ size_before = size
753
+ delete_if(&block)
754
+ size == size_before ? nil : self
755
+ end
756
+
757
+ # Keeps key-value pairs for which the block returns true, deleting the rest. Modifies the tree in place.
758
+ #
759
+ # @yield [key, value] each key-value pair
760
+ # @return [RBTree, Enumerator] self, or Enumerator if no block
761
+ def keep_if(&block)
762
+ return enum_for(__method__) { size } unless block_given?
763
+ each(safe: true) { |k, v| delete(k) unless block.call(k, v) }
764
+ self
765
+ end
766
+
767
+ # Deletes key-value pairs for which the block returns true. Modifies the tree in place.
768
+ #
769
+ # @yield [key, value] each key-value pair
770
+ # @return [RBTree, Enumerator] self, or Enumerator if no block
771
+ def delete_if(&block)
772
+ return enum_for(__method__) { size } unless block_given?
773
+ each(safe: true) { |k, v| delete(k) if block.call(k, v) }
774
+ self
775
+ end
776
+
777
+ # Returns a new tree with keys and values swapped.
778
+ #
779
+ # For RBTree, duplicate values result in later keys overwriting earlier ones.
780
+ # For MultiRBTree, all key-value pairs are preserved.
781
+ # Values must implement <=> to serve as keys in the new tree.
782
+ #
783
+ # @return [RBTree, MultiRBTree] a new tree with keys and values inverted
784
+ def invert
785
+ result = new_derived_tree
786
+ each { |k, v| result.insert(v, k) }
787
+ result
788
+ end
789
+
605
790
  # Returns a string representation of the tree.
606
791
  #
607
792
  # Shows the first 5 entries and total size. Useful for debugging.
@@ -613,19 +798,41 @@ class RBTree
613
798
  "#<#{self.class}:0x#{object_id.to_s(16)} size=#{size} {#{content}#{suffix}}>"
614
799
  end
615
800
 
616
- # Validates the red-black tree properties.
801
+ # Validates the red-black tree properties and the auxiliary structures.
617
802
  #
618
803
  # Checks that:
619
- # 1. Root is black
804
+ # 1. Root is black and has no parent
620
805
  # 2. All paths from root to leaves have the same number of black nodes
621
806
  # 3. No red node has a red child
622
- # 4. Keys are properly ordered
807
+ # 4. Keys are ordered against bounds inherited from ancestors
808
+ # 5. Every child's parent pointer points back at its parent
809
+ # 6. The Hash index holds exactly one entry per node, mapped to that node
810
+ # 7. `@min_node` / `@max_node` are the actual extremes, and `key_count` is right
623
811
  #
624
812
  # @return [Boolean] true if all properties are satisfied, false otherwise
625
813
  def valid?
626
814
  return false if @root.color == Node::RED
815
+ return false if @root != @nil_node && @root.parent != @nil_node
627
816
  return false if check_black_height(@root) == -1
628
- return false unless check_order(@root)
817
+ return false unless check_order(@root, nil, nil)
818
+ return false unless check_links(@root)
819
+
820
+ count = 0
821
+ first_node = nil
822
+ last_node = nil
823
+ indexed = true
824
+ each_node_asc do |n|
825
+ count += 1
826
+ first_node ||= n
827
+ last_node = n
828
+ indexed &&= @hash_index[n.key].equal?(n)
829
+ end
830
+
831
+ return false unless indexed
832
+ return false unless count == @key_count
833
+ return false unless @hash_index.size == @key_count
834
+ return false unless (first_node || @nil_node).equal?(@min_node)
835
+ return false unless (last_node || @nil_node).equal?(@max_node)
629
836
  true
630
837
  end
631
838
 
@@ -635,6 +842,108 @@ class RBTree
635
842
  def min_node = (@min_node == @nil_node) ? nil : @min_node
636
843
  def max_node = (@max_node == @nil_node) ? nil : @max_node
637
844
 
845
+ # Creates an empty tree of the same class, keeping this tree's allocator.
846
+ #
847
+ # @return [RBTree] a new empty tree
848
+ def new_derived_tree = self.class.new(node_allocator: @node_allocator)
849
+
850
+ # Locates the node holding the given key, by O(1) hash index or by ordering.
851
+ #
852
+ # The index is keyed by +eql?+ while the tree orders by <tt><=></tt>, so a key
853
+ # such as +1.0+ misses the index but still matches a stored +1+.
854
+ #
855
+ # @param key [Object] the key to locate
856
+ # @return [Node, nil] the node holding an equal key, or nil if absent
857
+ def find_node(key) = @hash_index[key] || find_node_by_order(key)
858
+
859
+ # Resolves a key the hash index missed, using the tree's <tt><=></tt> ordering.
860
+ #
861
+ # Split from {#find_node} so hot callers can inline the hash probe and pay no
862
+ # method call on a hit. The descent is skipped when every stored key and the
863
+ # lookup key share a class in {COHERENT_KEY_CLASSES}, since a match would then
864
+ # have been found in the index already. Incomparable keys report absence rather
865
+ # than raising, so lookups stay total.
866
+ #
867
+ # @param key [Object] the key to locate
868
+ # @return [Node, nil] the node holding an equal key, or nil if absent
869
+ def find_node_by_order(key)
870
+ return nil if @coherent_keys && key.instance_of?(@key_class)
871
+
872
+ x = @root
873
+ while x != @nil_node
874
+ cmp = key <=> x.key
875
+ return nil unless cmp
876
+ return x if cmp == 0
877
+ x = cmp < 0 ? x.left : x.right
878
+ end
879
+ nil
880
+ end
881
+
882
+ # Records the class of a newly inserted key for {#find_node_by_order}.
883
+ #
884
+ # Deletion never relaxes the result: a stale pessimistic value costs a descent,
885
+ # never correctness.
886
+ #
887
+ # @param key [Object] the key of the node being created
888
+ # @return [void]
889
+ def note_key_class(key)
890
+ klass = key.class
891
+ return if @key_class.equal?(klass)
892
+
893
+ if @key_class.nil?
894
+ @key_class = klass
895
+ @coherent_keys = COHERENT_KEY_CLASSES.include?(klass)
896
+ else
897
+ @key_class = false
898
+ @coherent_keys = false
899
+ end
900
+ end
901
+
902
+ # Compares two keys, rejecting incomparable ones with a clear error.
903
+ #
904
+ # @param a [Object] the left-hand key
905
+ # @param b [Object] the right-hand key
906
+ # @return [Integer] the result of <tt>a <=> b</tt>
907
+ # @raise [ArgumentError] if the keys cannot be compared
908
+ def compare!(a, b)
909
+ cmp = a <=> b
910
+ return cmp if cmp
911
+ raise ArgumentError, "comparison of #{a.class} with #{b.inspect} failed"
912
+ end
913
+
914
+ # Reports mutation detected during a non-safe traversal.
915
+ #
916
+ # Such a traversal walks live node links, and released nodes are recycled by the
917
+ # allocator, so mutating during one can yield keys out of order rather than
918
+ # merely skip them. Callers compare `@mod_count` inline and only call this on
919
+ # mismatch, keeping the per-element cost to an integer comparison.
920
+ #
921
+ # @raise [RuntimeError] always
922
+ # @return [void]
923
+ def concurrent_modification!
924
+ raise RuntimeError,
925
+ "can't modify #{self.class} during iteration (pass `safe: true` to iterate safely)"
926
+ end
927
+
928
+ # Walks every node in ascending key order, following live tree links.
929
+ #
930
+ # @yield [node] each node
931
+ # @return [void]
932
+ def each_node_asc
933
+ stack = []
934
+ current = @root
935
+ while current != @nil_node || !stack.empty?
936
+ if current != @nil_node
937
+ stack << current
938
+ current = current.left
939
+ else
940
+ current = stack.pop
941
+ yield current
942
+ current = current.right
943
+ end
944
+ end
945
+ end
946
+
638
947
  # Inserts a single key-value pair.
639
948
  #
640
949
  # @param key [Object] the key to insert
@@ -658,6 +967,9 @@ class RBTree
658
967
 
659
968
  # Generic entry insertion logic shared between RBTree and MultiRBTree.
660
969
  #
970
+ # A key that is <tt><=></tt>-equal to a stored key updates that node, which keeps
971
+ # the key object it was created with, so the index stays at one entry per node.
972
+ #
661
973
  # @param key [Object] the key to insert
662
974
  # @yield [node, is_new] yields the existing node (if any) and whether it's a new insertion
663
975
  # @yieldparam node [Node, nil] the existing node or nil
@@ -665,6 +977,7 @@ class RBTree
665
977
  # @yieldreturn [Object]
666
978
  # - if is_new: the initial value for the new node
667
979
  # - if !is_new: the value to return from insert_entry
980
+ # @raise [ArgumentError] if the key cannot be compared with the stored keys
668
981
  def insert_entry_generic(key)
669
982
  if (node = @hash_index[key])
670
983
  return yield(node, false)
@@ -675,6 +988,9 @@ class RBTree
675
988
  while x != @nil_node
676
989
  y = x
677
990
  cmp = key <=> x.key
991
+ unless cmp
992
+ raise ArgumentError, "comparison of #{key.class} with #{x.key.inspect} failed"
993
+ end
678
994
  if cmp == 0
679
995
  return yield(x, false)
680
996
  elsif cmp < 0
@@ -708,6 +1024,8 @@ class RBTree
708
1024
  end
709
1025
 
710
1026
  @hash_index[key] = z
1027
+ note_key_class(key)
1028
+ @mod_count += 1
711
1029
  true
712
1030
  end
713
1031
 
@@ -748,13 +1066,13 @@ class RBTree
748
1066
 
749
1067
  if safe
750
1068
  pair = !min ? find_min :
751
- include_min && @hash_index[min]&.pair || find_successor(min)
752
- while pair && (!max || pair[0] < max)
1069
+ include_min && find_node(min)&.pair || find_successor(min)
1070
+ while pair && (!max || (pair[0] <=> max) < 0)
753
1071
  current_key = pair[0]
754
1072
  yield pair
755
1073
  pair = find_successor(current_key)
756
1074
  end
757
- yield pair if pair && max && include_max && pair[0] == max
1075
+ yield pair if pair && max && include_max && (pair[0] <=> max) == 0
758
1076
  else
759
1077
  start_node, stack = resolve_startup_asc(min, include_min)
760
1078
  traverse_from_asc(start_node, stack, max, include_max, &block)
@@ -780,13 +1098,13 @@ class RBTree
780
1098
 
781
1099
  if safe
782
1100
  pair = !max ? find_max :
783
- include_max && @hash_index[max]&.pair || find_predecessor(max)
784
- while pair && (!min || pair[0] > min)
1101
+ include_max && find_node(max)&.pair || find_predecessor(max)
1102
+ while pair && (!min || (pair[0] <=> min) > 0)
785
1103
  current_key = pair[0]
786
1104
  yield pair
787
1105
  pair = find_predecessor(current_key)
788
1106
  end
789
- yield pair if pair && min && include_min && pair[0] == min
1107
+ yield pair if pair && min && include_min && (pair[0] <=> min) == 0
790
1108
  else
791
1109
  start_node, stack = resolve_startup_desc(max, include_max)
792
1110
  traverse_from_desc(start_node, stack, min, include_min, &block)
@@ -838,7 +1156,8 @@ class RBTree
838
1156
  return [@min_node, reconstruct_stack_asc(@min_node)]
839
1157
  end
840
1158
 
841
- # 2. Use Hash index if key exists
1159
+ # 2. Use Hash index if key exists. A pure O(1) probe, not {#find_node}: on a
1160
+ # miss, step 3's descent already places a <=>-equal node correctly.
842
1161
  if min && (node = @hash_index[min])
843
1162
  start_node = include_min ? node : successor_node_of(node)
844
1163
  return [start_node, reconstruct_stack_asc(start_node)]
@@ -884,16 +1203,21 @@ class RBTree
884
1203
  # @yieldparam key [Object] the key
885
1204
  # @yieldparam val [Object] the value
886
1205
  def traverse_from_asc(current, stack, max, include_max, &block)
1206
+ mod = @mod_count
887
1207
  while current != @nil_node || !stack.empty?
888
1208
  if current != @nil_node
889
1209
  if max
890
1210
  cmp = current.key <=> max
891
1211
  if cmp >= 0
892
- yield current.pair if include_max && cmp == 0
1212
+ if include_max && cmp == 0
1213
+ yield current.pair
1214
+ concurrent_modification! unless @mod_count == mod
1215
+ end
893
1216
  return
894
1217
  end
895
1218
  end
896
1219
  yield current.pair
1220
+ concurrent_modification! unless @mod_count == mod
897
1221
  current = current.right
898
1222
  while current != @nil_node
899
1223
  stack << current
@@ -916,7 +1240,7 @@ class RBTree
916
1240
  return [@max_node, reconstruct_stack_desc(@max_node)]
917
1241
  end
918
1242
 
919
- # 2. Use Hash index if key exists
1243
+ # 2. Use Hash index if key exists; see the note in #resolve_startup_asc.
920
1244
  if max && (node = @hash_index[max])
921
1245
  start_node = include_max ? node : predecessor_node_of(node)
922
1246
  return [start_node, reconstruct_stack_desc(start_node)]
@@ -962,16 +1286,21 @@ class RBTree
962
1286
  # @yieldparam key [Object] the key
963
1287
  # @yieldparam val [Object] the value
964
1288
  def traverse_from_desc(current, stack, min, include_min, &block)
1289
+ mod = @mod_count
965
1290
  while current != @nil_node || !stack.empty?
966
1291
  if current != @nil_node
967
1292
  if min
968
1293
  cmp = current.key <=> min
969
1294
  if cmp <= 0
970
- yield current.pair if include_min && cmp == 0
1295
+ if include_min && cmp == 0
1296
+ yield current.pair
1297
+ concurrent_modification! unless @mod_count == mod
1298
+ end
971
1299
  return
972
1300
  end
973
1301
  end
974
1302
  yield current.pair
1303
+ concurrent_modification! unless @mod_count == mod
975
1304
  current = current.left
976
1305
  while current != @nil_node
977
1306
  stack << current
@@ -1033,7 +1362,17 @@ class RBTree
1033
1362
  #
1034
1363
  # @param key [Object] the key to delete
1035
1364
  # @return [Object, nil] the value of the deleted node, or nil if not found
1036
- def delete_indexed_node(key) = (z = @hash_index.delete(key)) && delete_node(z)
1365
+ def delete_indexed_node(key) = (z = find_node(key)) && delete_found_node(z)
1366
+
1367
+ # Removes an already-located node, unindexing it under its own key so that a node
1368
+ # found through a non-+eql?+ but <tt><=></tt>-equal key is still removed correctly.
1369
+ #
1370
+ # @param z [Node] the node to remove
1371
+ # @return [Object] the value of the removed node
1372
+ def delete_found_node(z)
1373
+ @hash_index.delete(z.key)
1374
+ delete_node(z)
1375
+ end
1037
1376
 
1038
1377
  # Removes a node from the tree and restores red-black properties.
1039
1378
  #
@@ -1092,6 +1431,7 @@ class RBTree
1092
1431
 
1093
1432
  value = z.value
1094
1433
  release_node(z)
1434
+ @mod_count += 1
1095
1435
  value
1096
1436
  end
1097
1437
 
@@ -1229,8 +1569,9 @@ class RBTree
1229
1569
  # @return [Node] the predecessor node, or @nil_node if none exists
1230
1570
  def find_predecessor_node(key)
1231
1571
  # If key is larger than max_key, return max_node
1232
- return @max_node if max_key && (key <=> max_key) > 0
1233
- # If key exists using O(1) hash lookup, return predecessor node
1572
+ return @max_node if (mk = max_key) && compare!(key, mk) > 0
1573
+ # O(1) hash fast path; on a miss the descent below steps away from a
1574
+ # <=>-equal node in the right direction anyway.
1234
1575
  if (node = @hash_index[key])
1235
1576
  return predecessor_node_of(node)
1236
1577
  end
@@ -1269,10 +1610,10 @@ class RBTree
1269
1610
  # @return [Node] the successor node, or @nil_node if none exists
1270
1611
  def find_successor_node(key)
1271
1612
  # If key is larger than or equal to max_key, return nil
1272
- return @nil_node if max_key && (key <=> max_key) >= 0
1613
+ return @nil_node if (mk = max_key) && compare!(key, mk) >= 0
1273
1614
  # If key is smaller than min_key, return min_node
1274
- return @min_node if min_key && (key <=> min_key) < 0
1275
- # If key exists using O(1) hash lookup, return successor node
1615
+ return @min_node if (nk = min_key) && compare!(key, nk) < 0
1616
+ # O(1) hash fast path; see the note in #find_predecessor_node.
1276
1617
  if (node = @hash_index[key])
1277
1618
  return successor_node_of(node)
1278
1619
  end
@@ -1431,15 +1772,31 @@ class RBTree
1431
1772
  left_h + (node.color == Node::BLACK ? 1 : 0)
1432
1773
  end
1433
1774
 
1434
- def check_order(node)
1775
+ # Recursively checks the binary-search-tree ordering.
1776
+ #
1777
+ # Bounds are inherited from ancestors, so a key misplaced relative to a higher
1778
+ # ancestor is rejected even when it is ordered correctly against its parent.
1779
+ #
1780
+ # @param node [Node] the current node
1781
+ # @param lo [Object, nil] exclusive lower bound inherited from ancestors
1782
+ # @param hi [Object, nil] exclusive upper bound inherited from ancestors
1783
+ # @return [Boolean] true if the subtree is correctly ordered
1784
+ def check_order(node, lo, hi)
1785
+ return true if node == @nil_node
1786
+ return false if lo && (node.key <=> lo) <= 0
1787
+ return false if hi && (node.key <=> hi) >= 0
1788
+ check_order(node.left, lo, node.key) && check_order(node.right, node.key, hi)
1789
+ end
1790
+
1791
+ # Recursively checks that every child points back at its parent.
1792
+ #
1793
+ # @param node [Node] the current node
1794
+ # @return [Boolean] true if all parent links in the subtree are consistent
1795
+ def check_links(node)
1435
1796
  return true if node == @nil_node
1436
- if node.left != @nil_node && (node.left.key <=> node.key) >= 0
1437
- return false
1438
- end
1439
- if node.right != @nil_node && (node.right.key <=> node.key) <= 0
1440
- return false
1441
- end
1442
- check_order(node.left) && check_order(node.right)
1797
+ return false if node.left != @nil_node && !node.left.parent.equal?(node)
1798
+ return false if node.right != @nil_node && !node.right.parent.equal?(node)
1799
+ check_links(node.left) && check_links(node.right)
1443
1800
  end
1444
1801
  end
1445
1802
 
@@ -1508,6 +1865,11 @@ end
1508
1865
  # @author Masahito Suzuki
1509
1866
  # @since 0.1.2
1510
1867
  class MultiRBTree < RBTree
1868
+ # Sentinel for "no key given", so +nil+ and +false+ stay usable as keys.
1869
+ # @api private
1870
+ NO_KEY = Object.new
1871
+ private_constant :NO_KEY
1872
+
1511
1873
  def initialize(*args, **kwargs)
1512
1874
  @value_count = 0
1513
1875
  super
@@ -1516,38 +1878,90 @@ class MultiRBTree < RBTree
1516
1878
  # Returns the number of values stored in the tree.
1517
1879
  # @return [Integer] the number of values in the tree
1518
1880
  def size = @value_count
1881
+
1882
+ # Removes all elements from the tree.
1883
+ # @return [MultiRBTree] self
1884
+ def clear
1885
+ @value_count = 0
1886
+ super
1887
+ end
1888
+
1889
+ # Returns a Hash mapping each key to an Array of its values.
1890
+ #
1891
+ # Keys are inserted in ascending order, and each value Array is a fresh copy,
1892
+ # so mutating the result cannot corrupt the tree.
1893
+ #
1894
+ # @return [Hash] a new Hash with the tree's contents
1895
+ # @example
1896
+ # tree = MultiRBTree.new
1897
+ # tree.insert(1, 'a'); tree.insert(1, 'b')
1898
+ # tree.to_h # => {1 => ["a", "b"]}
1899
+ def to_h
1900
+ h = {}
1901
+ each { |k, v| (h[k] ||= []) << v }
1902
+ h
1903
+ end
1904
+
1905
+ # Validates everything `RBTree#valid?` does, plus that every node holds a
1906
+ # non-empty Array of values and that `size` matches the total value count.
1907
+ #
1908
+ # @return [Boolean] true if all properties are satisfied, false otherwise
1909
+ def valid?
1910
+ return false unless super
1911
+ total = 0
1912
+ ok = true
1913
+ each_node_asc do |n|
1914
+ ok &&= n.value.is_a?(Array) && !n.value.empty?
1915
+ total += n.value.size if n.value.is_a?(Array)
1916
+ end
1917
+ ok && total == @value_count
1918
+ end
1519
1919
 
1520
1920
  # Returns the minimum key-value pair without removing it.
1521
1921
  #
1922
+ # As with `RBTree#min`, a count or comparison block switches to `Enumerable#min`.
1923
+ #
1522
1924
  # @param last [Boolean] whether to return the last value (default: false)
1523
1925
  # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
1524
1926
  # @example
1525
1927
  # tree = MultiRBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
1526
1928
  # tree.min # => [1, "one"]
1527
- def min(last: false) = (n = min_node) && [n.key, n.value.send(last ? :last : :first)]
1929
+ def min(*args, last: false, &block)
1930
+ return super(*args, &block) if !args.empty? || block
1931
+ (n = min_node) && [n.key, n.value.send(last ? :last : :first)]
1932
+ end
1528
1933
 
1529
1934
  # Returns the maximum key-value pair without removing it.
1530
1935
  #
1936
+ # As with `RBTree#max`, a count or comparison block switches to `Enumerable#max`.
1937
+ #
1531
1938
  # @param last [Boolean] whether to return the last value (default: false)
1532
1939
  # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
1533
1940
  # @example
1534
1941
  # tree = MultiRBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
1535
1942
  # tree.max # => [3, "three"]
1536
- def max(last: false) = (n = max_node) && [n.key, n.value.send(last ? :last : :first)]
1943
+ def max(*args, last: false, &block)
1944
+ return super(*args, &block) if !args.empty? || block
1945
+ (n = max_node) && [n.key, n.value.send(last ? :last : :first)]
1946
+ end
1537
1947
 
1538
- # Returns the last key-value pair without removing it.
1948
+ # Returns the last key-value pair, or the last +n+ pairs, without removing them.
1539
1949
  #
1540
- # @return [Array(Object, Object), nil] a two-element array [key, value], or nil if tree is empty
1950
+ # @param n [Integer, nil] number of trailing pairs to return
1951
+ # @return [Array, nil] the pair, or the n last pairs in ascending order
1541
1952
  # @example
1542
1953
  # tree = MultiRBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
1543
1954
  # tree.last # => [3, "three"]
1544
- def last = max(last: true)
1955
+ def last(n = nil) = n.nil? ? max(last: true) : super(n)
1545
1956
 
1546
- # Returns the number of values for a given key or the total number of key-value pairs if no key is given.
1957
+ # Returns the number of values for a given key, or the total number of key-value
1958
+ # pairs when called without an argument.
1959
+ #
1960
+ # The default is a sentinel, so +nil+ and +false+ are looked up as ordinary keys.
1547
1961
  #
1548
- # @param key [Object, nil] the key to look up, or nil for total count
1962
+ # @param key [Object] the key to look up; omit for the total count
1549
1963
  # @return [Integer] the number of values for the key, or total count if no key is given
1550
- def value_count(key = nil) = !key ? size : (@hash_index[key]&.value&.size || 0)
1964
+ def value_count(key = NO_KEY) = key.equal?(NO_KEY) ? size : (find_node(key)&.value&.size || 0)
1551
1965
 
1552
1966
  # Retrieves a value associated with the given key.
1553
1967
  #
@@ -1560,7 +1974,7 @@ class MultiRBTree < RBTree
1560
1974
  # tree.insert(1, 'second')
1561
1975
  # tree.get(1) # => "first"
1562
1976
  # tree.get(1, last: true) # => "second"
1563
- def value(key, last: false) = @hash_index[key]&.value&.send(last ? :last : :first)
1977
+ def value(key, last: false) = (@hash_index[key] || find_node_by_order(key))&.value&.send(last ? :last : :first)
1564
1978
  alias :get :value
1565
1979
 
1566
1980
  # Retrieves the first value associated with the given key.
@@ -1580,15 +1994,17 @@ class MultiRBTree < RBTree
1580
1994
  # Retrieves all values associated with the given key.
1581
1995
  #
1582
1996
  # @param key [Object] the key to look up
1583
- # @return [Array, nil] an Array containing all values, or nil if not found
1997
+ # @param reverse [Boolean] if true, yield the values in reverse insertion order (default: false)
1998
+ # @return [Enumerator, nil] an Enumerator over the values, or nil if the key is absent (when a block is given)
1584
1999
  # @example
1585
2000
  # tree = MultiRBTree.new
1586
2001
  # tree.insert(1, 'first')
1587
2002
  # tree.insert(1, 'second')
1588
- # tree.values(1).to_a # => ["first", "second"]
2003
+ # tree.values(1).to_a # => ["first", "second"]
2004
+ # tree.values(1, reverse: true).to_a # => ["second", "first"]
1589
2005
  def values(key, reverse: false)
1590
- return enum_for(__method__, key) { value_count(key) } unless block_given?
1591
- @hash_index[key]&.value&.send(reverse ? :reverse_each : :each) { |v| yield v }
2006
+ return enum_for(__method__, key, reverse: reverse) { value_count(key) } unless block_given?
2007
+ find_node(key)&.value&.send(reverse ? :reverse_each : :each) { |v| yield v }
1592
2008
  end
1593
2009
  alias :get_all :values
1594
2010
 
@@ -1648,9 +2064,13 @@ class MultiRBTree < RBTree
1648
2064
  # tree.delete_value(1) # => "first"
1649
2065
  # tree.delete_value(1, last: true) # => "second" (if more values existed)
1650
2066
  def delete_value(key, last: false)
1651
- (z = @hash_index[key]) or return nil
2067
+ (z = find_node(key)) or return nil
1652
2068
  value = z.value.send(last ? :pop : :shift)
1653
- z.value.empty? && delete_indexed_node(key)
2069
+ if z.value.empty?
2070
+ delete_found_node(z)
2071
+ else
2072
+ @mod_count += 1
2073
+ end
1654
2074
  @value_count -= 1
1655
2075
  value
1656
2076
  end
@@ -1683,9 +2103,9 @@ class MultiRBTree < RBTree
1683
2103
  # vals = tree.delete(1) # removes both values
1684
2104
  # vals.size # => 2
1685
2105
  def delete_key(key)
1686
- return nil unless (z = @hash_index[key])
2106
+ return nil unless (z = find_node(key))
1687
2107
  @value_count -= (value = z.value).size
1688
- delete_indexed_node(z.key)
2108
+ delete_found_node(z)
1689
2109
  value
1690
2110
  end
1691
2111
  alias :delete :delete_key
@@ -1697,9 +2117,14 @@ class MultiRBTree < RBTree
1697
2117
  # tree = MultiRBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
1698
2118
  # tree.shift # => [1, "one"]
1699
2119
  def shift
1700
- (key, vals = min_node&.pair) or return nil
2120
+ (n = min_node) or return nil
2121
+ key, vals = n.pair
1701
2122
  val = vals.shift
1702
- vals.empty? && delete_indexed_node(key)
2123
+ if vals.empty?
2124
+ delete_found_node(n)
2125
+ else
2126
+ @mod_count += 1
2127
+ end
1703
2128
  @value_count -= 1
1704
2129
  [key, val]
1705
2130
  end
@@ -1711,16 +2136,61 @@ class MultiRBTree < RBTree
1711
2136
  # tree = MultiRBTree.new({3 => 'three', 1 => 'one', 2 => 'two'})
1712
2137
  # tree.pop # => [3, "three"]
1713
2138
  def pop
1714
- (key, vals = max_node&.pair) or return nil
2139
+ (n = max_node) or return nil
2140
+ key, vals = n.pair
1715
2141
  val = vals.pop
1716
- vals.empty? && delete_indexed_node(key)
2142
+ if vals.empty?
2143
+ delete_found_node(n)
2144
+ else
2145
+ @mod_count += 1
2146
+ end
1717
2147
  @value_count -= 1
1718
2148
  [key, val]
1719
2149
  end
1720
2150
 
2151
+ # Keeps key-value pairs for which the block returns true, deleting the rest.
2152
+ # Unlike RBTree, this removes individual values rather than entire keys.
2153
+ #
2154
+ # @yield [key, value] each key-value pair
2155
+ # @return [MultiRBTree, Enumerator] self, or Enumerator if no block
2156
+ def keep_if(&block)
2157
+ return enum_for(__method__) { size } unless block_given?
2158
+ filter_values! { |k, v| block.call(k, v) }
2159
+ self
2160
+ end
2161
+
2162
+ # Deletes key-value pairs for which the block returns true.
2163
+ # Unlike RBTree, this removes individual values rather than entire keys.
2164
+ #
2165
+ # @yield [key, value] each key-value pair
2166
+ # @return [MultiRBTree, Enumerator] self, or Enumerator if no block
2167
+ def delete_if(&block)
2168
+ return enum_for(__method__) { size } unless block_given?
2169
+ filter_values! { |k, v| !block.call(k, v) }
2170
+ self
2171
+ end
2172
+
1721
2173
  # @!visibility private
1722
2174
  private
1723
2175
 
2176
+ # Filters values in-place across all nodes.
2177
+ # Keeps only values for which the block returns true.
2178
+ # Removes nodes whose value arrays become empty.
2179
+ # Updates @value_count accordingly.
2180
+ def filter_values!
2181
+ emptied = []
2182
+ @hash_index.each_value do |node|
2183
+ before = node.value.size
2184
+ node.value.select! { |v| yield node.key, v }
2185
+ removed = before - node.value.size
2186
+ next if removed == 0
2187
+ @value_count -= removed
2188
+ @mod_count += 1
2189
+ emptied << node if node.value.empty?
2190
+ end
2191
+ emptied.each { |node| delete_found_node(node) }
2192
+ end
2193
+
1724
2194
  # Inserts a value for the given key.
1725
2195
  #
1726
2196
  # If the key already exists, the value is appended to its list.
@@ -1741,6 +2211,7 @@ class MultiRBTree < RBTree
1741
2211
  [value]
1742
2212
  else
1743
2213
  node.value << value
2214
+ @mod_count += 1 # observable to a traversal walking this value list
1744
2215
  true
1745
2216
  end
1746
2217
  end
@@ -1748,22 +2219,49 @@ class MultiRBTree < RBTree
1748
2219
 
1749
2220
  # Traverses the tree in ascending order, yielding each key-value pair.
1750
2221
  #
1751
- # @param range [Range] the range of keys to traverse
2222
+ # Each node's value list is flattened to one [key, value] pair per value. Safe
2223
+ # mode snapshots the list, so removing values of the key being iterated cannot
2224
+ # skip its siblings, though such values may still be yielded.
2225
+ #
2226
+ # @param min [Object] the lower bound
2227
+ # @param max [Object] the upper bound
2228
+ # @param include_min [Boolean] whether to include the lower bound
2229
+ # @param include_max [Boolean] whether to include the upper bound
2230
+ # @param safe [Boolean] whether to use safe traversal
1752
2231
  # @yield [Array(Object, Object)] each key-value pair
1753
- # @yieldparam key [Object] the key
1754
- # @yieldparam val [Object] the value
1755
- def traverse_range_asc(...)
1756
- super { |k, vals| vals.each { |v| yield [k, v] } }
2232
+ def traverse_range_asc(min, max, include_min, include_max, safe: false, &block)
2233
+ if safe
2234
+ super(min, max, include_min, include_max, safe: true) do |k, vals|
2235
+ vals.dup.each { |v| yield [k, v] }
2236
+ end
2237
+ else
2238
+ mod = @mod_count
2239
+ super(min, max, include_min, include_max, safe: false) do |k, vals|
2240
+ vals.each { |v| yield [k, v]; concurrent_modification! unless @mod_count == mod }
2241
+ end
2242
+ end
1757
2243
  end
1758
2244
 
1759
2245
  # Traverses the tree in descending order, yielding each key-value pair.
1760
2246
  #
1761
- # @param range [Range] the range of keys to traverse
2247
+ # @param min [Object] the lower bound
2248
+ # @param max [Object] the upper bound
2249
+ # @param include_min [Boolean] whether to include the lower bound
2250
+ # @param include_max [Boolean] whether to include the upper bound
2251
+ # @param safe [Boolean] whether to use safe traversal
1762
2252
  # @yield [Array(Object, Object)] each key-value pair
1763
- # @yieldparam key [Object] the key
1764
- # @yieldparam val [Object] the value
1765
- def traverse_range_desc(...)
1766
- super { |k, vals| vals.reverse_each { |v| yield [k, v] } }
2253
+ # @see #traverse_range_asc
2254
+ def traverse_range_desc(min, max, include_min, include_max, safe: false, &block)
2255
+ if safe
2256
+ super(min, max, include_min, include_max, safe: true) do |k, vals|
2257
+ vals.dup.reverse_each { |v| yield [k, v] }
2258
+ end
2259
+ else
2260
+ mod = @mod_count
2261
+ super(min, max, include_min, include_max, safe: false) do |k, vals|
2262
+ vals.reverse_each { |v| yield [k, v]; concurrent_modification! unless @mod_count == mod }
2263
+ end
2264
+ end
1767
2265
  end
1768
2266
  end
1769
2267
 
@@ -1834,6 +2332,15 @@ class RBTree::NodeAllocator
1834
2332
  #
1835
2333
  # @param node [Node] the node to release
1836
2334
  def release(node) = nil
2335
+
2336
+ # Notifies the allocator that +count+ nodes were dropped in bulk rather than
2337
+ # released individually, as `RBTree#clear` does.
2338
+ #
2339
+ # The nodes are not reclaimed for reuse; this only keeps live-node statistics right.
2340
+ #
2341
+ # @param count [Integer] the number of nodes discarded
2342
+ # @return [void]
2343
+ def discard(count) = nil
1837
2344
  end
1838
2345
 
1839
2346
  # Internal node pool for RBTree.
@@ -1947,12 +2454,22 @@ class RBTree::AutoShrinkNodePool < RBTree::NodePool
1947
2454
  @min_active_in_interval = @active_nodes if @active_nodes < @min_active_in_interval
1948
2455
 
1949
2456
  @check_count += 1
1950
-
2457
+
1951
2458
  perform_maintenance if @check_count >= @check_interval
1952
2459
 
1953
2460
  super if @pool.size < @current_target_capacity
1954
2461
  end
1955
2462
 
2463
+ # Accounts for nodes dropped in bulk, so that the live-node count comes back down
2464
+ # after a `clear` instead of inflating the observed fluctuation range.
2465
+ #
2466
+ # @param count [Integer] the number of nodes discarded
2467
+ # @return [void]
2468
+ def discard(count)
2469
+ @active_nodes -= count
2470
+ @min_active_in_interval = @active_nodes if @active_nodes < @min_active_in_interval
2471
+ end
2472
+
1956
2473
  private
1957
2474
 
1958
2475
  def perform_maintenance