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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d89cd253c189b3342f30e2a4df3a293561e2353af6592d86f25fcea5eeb76433
4
- data.tar.gz: 9af04bff3851e7806fc637c2fc0ad583decf8e7c020edf841220a2b90fe8aaf4
3
+ metadata.gz: d1b75c1ac61280ad120b20c9e945e8ac0890e8732905d0699229c57606ae01c3
4
+ data.tar.gz: caaa60ee759ade14c62f169d0126a50c38e344259918baba96e3a24dcf1a57f1
5
5
  SHA512:
6
- metadata.gz: a9708ffb8b421f883953c68582c607b9333e671542e21c0f3e0927838d2bb68202142150c11dd11541ea79fa1fcafc6c3195456c38d4b0db6b3897272d205835
7
- data.tar.gz: 601190e616b7b478cc6ceabe682e9d43ef169871753667fe61e326f65daceaa60441122a550f8458f1d4a7a794a032ac0add88f326956db903854ee96e34b613
6
+ metadata.gz: 8316e96b9f44660dbba226688874629571d48059c728d057476d6dc47704ee97a521fe4850277cd2cacea44cdedbd0b26014f7811b339cb8aa0738306bd3fa12
7
+ data.tar.gz: 6747969000f0293210656e06c5be5068cb2db668f8dffc4c6a23b87202081e98b46ce84d9369bc57b69c2f4e19b2e4c33538b06e83384ce3e25e5e5aea518a8e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,128 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-08-15
9
+
10
+ A correctness release. Every defect fixed here was present in 0.3.6 with a fully
11
+ green test suite, so each one now has a regression test that fails against 0.3.6.
12
+
13
+ ### Fixed
14
+
15
+ - **Key identity between the tree and the hash index**: the internal index keys on
16
+ `eql?`/`hash` while the tree orders by `<=>`, so a key that was `<=>`-equal to a
17
+ stored key but not `eql?` to it (most commonly `1.0` against a stored `1`) could be
18
+ written but not read back: `tree[1] = 'a'; tree[1.0]` returned `nil`, and
19
+ `tree.delete(1.0)` was a no-op. Lookups that miss the hash index now fall back to an
20
+ O(log n) tree search, so ordering and lookup can no longer disagree. Lookups of keys
21
+ that cannot be compared at all report absence instead of raising. That fallback is
22
+ skipped when it is provably futile — see `RBTree.coherent_key_class` below — so
23
+ ordinary key types keep O(1) lookups for hits and misses alike.
24
+ - **`delete` / `delete_key` with `nil` or `false` values**: presence was decided by the
25
+ truthiness of the value, so entries holding `nil` or `false` could not be removed.
26
+ `shift` and `pop` returned such a pair without removing it, which made the idiomatic
27
+ `while (pair = tree.shift)` loop spin forever. `delete_if`, `keep_if` and `reject!`
28
+ were affected for the same reason.
29
+ - **`to_h` ignored the tree's order**: it was built from the hash index, so it came back
30
+ in insertion order rather than key order. It is now built by ordered traversal, and
31
+ `MultiRBTree#to_h` returns copies of the value lists instead of the live arrays.
32
+ - **`MultiRBTree#merge!(self)` and `insert(self)` never terminated**: appending to a
33
+ key's value list while iterating that same list grew it without bound. Self-insertion
34
+ now operates on a snapshot, so `tree.merge!(tree)` doubles each entry as expected.
35
+ - **`MultiRBTree#keys` enumerator size**: `keys.to_a` yields one entry per value while
36
+ `keys.size` reported the number of unique keys. The size now matches what is yielded.
37
+ - **`MultiRBTree#values(key, reverse: true)`** silently ignored `reverse:` when called
38
+ without a block; the returned Enumerator now honours it.
39
+ - **`MultiRBTree#value_count(key)`** treated a `false` or `nil` key as "no argument" and
40
+ returned the total count. A sentinel default is used instead, so `nil` and `false` are
41
+ looked up as ordinary keys.
42
+ - **Safe-mode range queries required `Comparable`**: the safe traversal compared bounds
43
+ with `<` and `>` while the rest of the code used `<=>`, so keys defining only `<=>`
44
+ raised `NoMethodError` under `safe: true`, and boundary elements could be dropped.
45
+ - **`clear` corrupted node-pool statistics**: discarded nodes were never accounted for,
46
+ so `AutoShrinkNodePool`'s live-node count only ever grew, permanently inflating the
47
+ pool's target capacity in a build/clear cycle. `clear` stays O(1) and now notifies the
48
+ allocator through the new `NodeAllocator#discard` hook.
49
+ - **`dup`, `select`, `reject`, `invert` and `merge` dropped a custom allocator**, silently
50
+ reverting the result to a default `AutoShrinkNodePool`. They now carry the allocator over.
51
+ - **Ordering queries raised `NoMethodError` on incomparable keys** (`undefined method '>'
52
+ for nil`); they raise `ArgumentError` with a useful message instead.
53
+
54
+ ### Added
55
+
56
+ - **Modification detection during iteration**: mutating the tree inside a non-safe
57
+ iteration now raises `RuntimeError`, as `Hash` does. This previously produced silent
58
+ corruption rather than merely skipping elements — because released nodes are recycled
59
+ by the allocator, an in-flight traversal could be rewired and emit keys **out of
60
+ order**. The check covers `each`, `reverse_each`, `keys`, every range query, and any
61
+ `Enumerator` they return. Replacing the value of an existing key is not a structural
62
+ change and remains allowed; `safe: true` is unaffected.
63
+ - **`Enumerable`-compatible `first`, `last`, `min` and `max`**: `first(n)` and `min(n)`
64
+ raised `ArgumentError`, and a comparison block passed to `min`/`max` was silently
65
+ ignored — returning the tree's extreme rather than the block's. With no argument and
66
+ no block these remain O(1) cached lookups; otherwise `Enumerable` semantics apply.
67
+ `last(n)` returns the n largest pairs in ascending order.
68
+ - **`RBTree#valid?` now verifies the auxiliary structures**, not just the red-black
69
+ invariants: hash-index agreement, `min`/`max` caches, `key_count`, parent pointers,
70
+ and — in `MultiRBTree` — `value_count` and the per-node value lists. Ordering is
71
+ checked against bounds inherited from ancestors, so a key misplaced across subtrees is
72
+ detected rather than only one misplaced against its immediate parent.
73
+ - **`RBTree.coherent_key_class(klass)`**: declares that a key class resolves
74
+ `<=>`-equality and `eql?` identically, letting lookups that miss the hash index skip
75
+ the fallback tree descent. `Integer`, `Float`, `Rational`, `String`, `Symbol` and
76
+ `Time` are known coherent and need no declaration; each was verified against its corner
77
+ cases (`0.0` vs `-0.0`, Strings differing only in encoding, `Time` values in different
78
+ zones, Integers beyond the fixnum range). Trees whose keys are all of one such class
79
+ therefore pay essentially nothing for the correctness fix above: 50,000 missing lookups
80
+ on a 200,000-entry Integer-keyed tree cost 8.1 ms, against 6.3 ms for a bare hash probe
81
+ and 72 ms with an unconditional descent. Mixed-class trees and undeclared custom key
82
+ classes keep the descent and stay correct.
83
+ - **`NodeAllocator#discard(count)`** hook for bulk node disposal.
84
+ - **`rake test`** task (the Rakefile previously defined none).
85
+
86
+ ### Changed
87
+
88
+ - `MultiRBTree#to_h` returns `{key => [values]}` with copied arrays; mutating the result
89
+ no longer affects the tree.
90
+ - `MultiRBTree` safe iteration snapshots each key's value list, so removing values during
91
+ the iteration of their own key no longer skips siblings. Values removed during that
92
+ key's iteration may still be yielded.
93
+ - A key that is `<=>`-equal to a stored key updates the existing entry, and the node keeps
94
+ the key object it was first created with (its canonical key).
95
+ - `dup` shares the original's allocator instance rather than creating a default one.
96
+
97
+ ### Notes on compatibility
98
+
99
+ - Code that mutated a tree during a non-safe iteration and appeared to work will now
100
+ raise; add `safe: true`.
101
+ - `to_h` key order changed from insertion order to ascending key order.
102
+ - `MultiRBTree#value_count(nil)` now looks up the key `nil` instead of returning the
103
+ total; call `value_count` with no arguments for the total.
104
+ - Lookups of absent keys are O(log n) rather than O(1) *only* on trees that mix key
105
+ classes or are keyed by an undeclared custom class; on a tree keyed by a single core
106
+ class every lookup stays O(1). Lookups that hit the hash index are unchanged.
107
+
108
+ ## [0.3.6] - 2026-01-28
109
+
110
+ ### Added
111
+
112
+ > **Note:** All methods added in this release are **convenience methods** composed from existing public API primitives (`each`, `insert`, `delete`, `dup`, `merge!`, `safe: true`). They provide no speed advantage over manual composition — their value is purely in readability and API completeness.
113
+
114
+ - **`dup` (Deep Copy)**: `RBTree` and `MultiRBTree` now support `dup` and `clone` via `initialize_copy`. Creates an independent deep copy of the tree — modifications to the copy do not affect the original and vice versa.
115
+ - **`select`**: Returns a new tree containing only key-value pairs for which the block returns true. Returns an `Enumerator` if no block is given.
116
+ - **`reject`**: Returns a new tree excluding key-value pairs for which the block returns true. Returns an `Enumerator` if no block is given.
117
+ - **`delete_if`**: Deletes key-value pairs in place for which the block returns true. Returns self. In `MultiRBTree`, operates at individual value granularity (can remove specific values without deleting the entire key).
118
+ - **`reject!`**: Same as `delete_if`, but returns `nil` if no changes were made.
119
+ - **`keep_if`**: Keeps only key-value pairs for which the block returns true, deleting the rest in place. Returns self. In `MultiRBTree`, operates at individual value granularity.
120
+ - **`invert`**: Returns a new tree with keys and values swapped. For `RBTree`, duplicate values result in the last key winning (same as `Hash#invert`). For `MultiRBTree`, all pairs are preserved.
121
+ - **`merge`** (non-destructive): Returns a new tree with merged contents. Supports block for duplicate key resolution: `tree.merge(other) { |key, v1, v2| v1 }`.
122
+ - **`merge!` block support**: `merge!` now accepts a block for duplicate key resolution, matching `Hash#merge!` behavior.
123
+
124
+ ### Optimized
125
+ - **`MultiRBTree#delete_if` / `#keep_if`**: Replaced clear-and-rebuild approach with in-place value array filtering via internal `filter_values!`. Avoids O(n log n) tree reconstruction; now runs in O(n) by directly mutating each node's value array and removing only emptied nodes.
126
+
127
+ ### Fixed
128
+ - **`MultiRBTree#clear`**: Fixed `@value_count` not being reset when `clear` was called, which caused `size` to return incorrect values after clearing and re-inserting.
129
+
8
130
  ## [0.3.5] - 2026-01-26
9
131
 
10
132
  ### Optimized
@@ -230,6 +352,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
230
352
  - ASCII diagrams for tree rotation operations
231
353
  - MIT License (Copyright © 2026 Masahito Suzuki)
232
354
 
355
+ [0.4.0]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.4.0
356
+ [0.3.6]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.6
233
357
  [0.3.5]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.5
234
358
  [0.3.4]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.4
235
359
  [0.3.3]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.3
data/README.ja.md CHANGED
@@ -10,10 +10,10 @@ Red-Black Tree(赤黒木)データ構造のピュアRuby実装です。挿
10
10
  - **順序付き操作**: ソート済みイテレーション、範囲クエリ(lt, gt, between)、最小/最大値取得が高速に実行可能。
11
11
  - **複数値サポート**: `MultiRBTree`クラスで同一キーに複数の値を格納。値は挿入順に保持され、最初または最後の値を個別にアクセス可能。
12
12
  - **ピュアRuby**: C拡張不要。MRI, JRuby, TruffleRubyなどあらゆるRuby実装で動作。
13
- - **ハイブリッドインデックス**: 内部ハッシュインデックスにより、キー検索と存在確認がO(1)の超高速アクセスを実現。
13
+ - **ハイブリッドインデックス**: 内部ハッシュインデックスにより、キー検索と存在確認がO(1)の超高速アクセスを実現。同時にキーの同一性は順序が決めるため、検索結果と順序判定が食い違うことはありません([キーの要件](#キーの要件)参照)。
14
14
  - **メモリ効率**: ノードプールによるオブジェクト再利用と自動縮小機能でGC負荷を大幅に削減。長時間実行アプリにも適応。
15
15
  - **最近傍検索**: 数値キーに対して、最も近いキーペアをO(log n)で効率的に探索。
16
- - **安全なイテレーション**: `safe: true`オプションにより、イテレーション中に他の操作(削除・挿入)を安全に実行可能。
16
+ - **安全なイテレーション**: `safe: true`オプションにより、イテレーション中に他の操作(削除・挿入)を安全に実行可能。指定しない場合、イテレーション中の変更は黙って壊れるのではなく `RuntimeError` になります。
17
17
 
18
18
  ## インストール
19
19
 
@@ -71,12 +71,16 @@ tree.each { |key, value| puts "#{key}: #{value}" }
71
71
  # イテレーション中の変更
72
72
  # 標準のHashやArrayとは異なり、`safe: true`オプションを指定することで
73
73
  # イテレーション中に安全にキーの削除や挿入を行うことができます。
74
+ # 指定しない場合、イテレーション中の変更は RuntimeError になります(「イテレーションの安全性」参照)。
74
75
  tree.each(safe: true) { |k, v| tree.delete(k) if k.even? }
75
76
  tree.each(reverse: true) { |k, v| puts k } # reverse_eachと同じ
76
77
 
77
- # 最小値と最大値
78
+ # 最小値と最大値 — O(1)。個数やブロックを渡した場合は Enumerable 互換の動作になります
78
79
  tree.min # => [1, "one"]
79
80
  tree.max # => [20, "twenty"]
81
+ tree.first(2) # => [[1, "one"], [2, "two"]]
82
+ tree.last(2) # => [[10, "ten"], [20, "twenty"]]
83
+ tree.min(2) # => [[1, "one"], [2, "two"]]
80
84
 
81
85
  # 範囲クエリ(Enumeratorを返す、配列には.to_aを使用)
82
86
  tree.lt(10).to_a # => [[1, "one"], [2, "two"], [3, "three"]]
@@ -194,13 +198,66 @@ tree = RBTree.new({1 => 'one', 2 => 'two'})
194
198
  # 配列への変換(Enumerable経由)
195
199
  tree.to_a # => [[1, "one"], [2, "two"]]
196
200
 
197
- # ハッシュへの変換
201
+ # ハッシュへの変換(キーは昇順に挿入されます)
198
202
  tree.to_h # => {1 => "one", 2 => "two"}
199
203
 
200
- # 他のツリー、ハッシュ、またはEnumerableの結合
204
+ # MultiRBTree は各キーに値リストのコピーを対応させます
205
+ multi = MultiRBTree.new
206
+ multi.insert(1, 'a'); multi.insert(1, 'b')
207
+ multi.to_h # => {1 => ["a", "b"]}
208
+
209
+ # 結合(破壊的)
201
210
  other = {3 => 'three'}
202
211
  tree.merge!(other)
203
212
  tree.size # => 3
213
+
214
+ # 結合(非破壊) — 新しいツリーを返す
215
+ merged = tree.merge({4 => 'four'})
216
+
217
+ # ブロックで重複キーの解決
218
+ merged = tree.merge({1 => 'ONE'}) { |key, old_val, new_val| old_val }
219
+
220
+ # キーと値を入れ替え
221
+ tree = RBTree.new({1 => 'a', 2 => 'b', 3 => 'c'})
222
+ tree.invert.to_a # => [["a", 1], ["b", 2], ["c", 3]]
223
+ ```
224
+
225
+ ### フィルタリングとコピー
226
+
227
+ > **注記:** `dup`、`select`、`reject`、`delete_if`、`reject!`、`keep_if`、`invert`、`merge` は既存のプリミティブを組み合わせた利便性メソッドです。手動で同等の処理を書いた場合と比べて速度上の利点はありません。可読性とAPI互換性のために提供されています。
228
+
229
+ Rubyの慣習的なメソッドでツリーのコピーやフィルタリングが可能です:
230
+
231
+ ```ruby
232
+ tree = RBTree.new({1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'})
233
+
234
+ # ディープコピー — 元のツリーと独立
235
+ copy = tree.dup
236
+ copy.delete(1)
237
+ tree.size # => 4 (変更なし)
238
+
239
+ # select / reject — 新しいツリーを返す
240
+ evens = tree.select { |k, _| k.even? } # => {2=>"two", 4=>"four"}
241
+ odds = tree.reject { |k, _| k.even? } # => {1=>"one", 3=>"three"}
242
+
243
+ # delete_if / keep_if — 破壊的に変更
244
+ tree.delete_if { |k, _| k > 2 }
245
+ tree.to_a # => [[1, "one"], [2, "two"]]
246
+
247
+ # reject! — delete_ifと同様だが、変更がなければnilを返す
248
+ tree.reject! { |_, _| false } # => nil
249
+ ```
250
+
251
+ `MultiRBTree`では、`delete_if`と`keep_if`は個々の値単位で操作します:
252
+
253
+ ```ruby
254
+ tree = MultiRBTree.new
255
+ tree.insert(1, 'a')
256
+ tree.insert(1, 'b')
257
+ tree.insert(2, 'c')
258
+
259
+ tree.delete_if { |k, v| k == 1 && v == 'a' }
260
+ tree.to_a # => [[1, "b"], [2, "c"]] — 'a'のみ削除された
204
261
  ```
205
262
 
206
263
  ### MultiRBTree 値配列アクセス
@@ -231,20 +288,110 @@ tree.min # => [1, "second"] (最小キーの最初の値)
231
288
  tree.max(last: true) # => [2, "b"] (最大キーの最後の値)
232
289
  ```
233
290
 
291
+ ## セマンティクス
292
+
293
+ ### キーの要件
294
+
295
+ キーは `<=>` で相互に比較可能である必要があります。キーの同一性を決めるのは `eql?`/`hash`
296
+ ではなく**順序**です。つまり `(key <=> stored_key) == 0` となるキーは、すべて同一のエントリを指します。
297
+
298
+ ```ruby
299
+ tree = RBTree.new
300
+ tree[1] = 'int'
301
+
302
+ tree[1.0] # => "int" (1.0 <=> 1 が 0 なので同じキー)
303
+ tree.has_key?(1.0) # => true
304
+ tree[1.0] = 'float'
305
+ tree.to_a # => [[1, "float"]] — 最初に挿入されたキーオブジェクトが保持される
306
+ ```
307
+
308
+ 内部のハッシュインデックスは `eql?`/`hash` をキーにしているため、上記の `tree[1.0]` は
309
+ インデックスにヒットせず、木を使って解決されます。ただしこのフォールバックは、**無駄だと
310
+ 証明できる場合には省略されます** — 格納済みの全キーと検索キーが同一クラスで、そのクラスにおいて
311
+ `<=>` 等価と `eql?` が一致するなら、該当するキーは既にインデックスで見つかっているはずだからです。
312
+ `Integer` / `Float` / `Rational` / `String` / `Symbol` / `Time` がこれに該当します:
313
+
314
+ | ツリー | 検索コスト |
315
+ |---|---|
316
+ | 全キーが上記コアクラスのいずれか1種類 | **O(1)**(ヒットもミスも) |
317
+ | クラス混在、または独自キークラス | ハッシュヒット時 O(1)、それ以外 O(log n) |
318
+
319
+ `<=>` に加えて整合する `eql?`/`hash` を定義した独自クラスは、O(1) 経路に参加できます:
320
+
321
+ ```ruby
322
+ class Version
323
+ include Comparable
324
+ attr_reader :parts
325
+ def initialize(parts) = @parts = parts
326
+ def <=>(other) = parts <=> other.parts
327
+ def eql?(other) = other.instance_of?(Version) && parts == other.parts
328
+ def hash = parts.hash
329
+ end
330
+
331
+ RBTree.coherent_key_class(Version)
332
+ ```
333
+
334
+ これは純粋な最適化ヒントです。宣言しなくても正しく動作し、インデックスを外れた検索が
335
+ O(log n) の木探索になるだけです。`<=>` だけを定義して `eql?`/`hash` を既定(同一性)の
336
+ ままにしているクラスは、**宣言してはいけません**。
337
+
338
+ 比較不能なキーは、例外ではなく「存在しない」として扱われます:
339
+
340
+ ```ruby
341
+ tree['Integer と <=> できない値'] # => nil
342
+ tree.has_key?('...') # => false
343
+ tree['...'] = 'x' # ArgumentError — 挿入には順序が必要
344
+ ```
345
+
346
+ ### イテレーションの安全性
347
+
348
+ イテレーションは生のノードリンクをたどり、削除されたノードはアロケータによって再利用されます。
349
+ そのためイテレーション中の変更は「要素を取りこぼす」だけでは済まず、**キーが順序を無視して
350
+ yield されうる**という壊れ方をします。これは検出され、明示的なエラーになります:
351
+
352
+ ```ruby
353
+ tree.each { |k, v| tree.delete(k) }
354
+ # => RuntimeError: can't modify RBTree during iteration (pass `safe: true` to iterate safely)
355
+
356
+ tree.each(safe: true) { |k, v| tree.delete(k) } # サポートされている
357
+ ```
358
+
359
+ この検出は `each` / `reverse_each` / `keys` / 全ての範囲クエリと、それらが返す `Enumerator`
360
+ (後から変更された場合を含む)に適用されます。既存キーの値の差し替えは構造変更ではないため
361
+ 許可されます。`delete_if` / `keep_if` / `reject!` は内部で安全にイテレートします。
362
+
363
+ `MultiRBTree` の `safe: true` は各キーの値リストのスナップショットを取ります。そのため、
364
+ あるキーのイテレーション中にそのキーの値を削除しても兄弟要素が飛ばされることはありません。
365
+ 一方で、自分のキーのイテレーション中に削除された値は yield されることがあります。
366
+
367
+ ### スレッド安全性
368
+
369
+ いかなる操作も同期されていません。木・ハッシュインデックス・ノードアロケータはすべて共有される
370
+ 可変状態であるため、複数スレッドから触る場合は外部でロックしてください。なお、複数の木に渡した
371
+ ノードアロケータ(`dup` 経由を含む)は、それらの木の間で共有される状態になります。
372
+
234
373
  ## パフォーマンス
235
374
 
236
375
  主要な操作は**O(log n)**時間で実行:
237
376
 
238
377
  - `insert(key, value)` - O(log n)
239
378
  - `delete(key)` - O(log n)
240
- - `value(key)` / `[]` - **O(1)** (内部ハッシュインデックスによる超高速アクセス)
241
- - `has_key?` - **O(1)** (内部ハッシュインデックスによる超高速チェック)
379
+ - `value(key)` / `[]` - **O(1)**(クラス混在・独自キーのツリーでミスした場合のみ O(log n)。[キーの要件](#キーの要件)参照)
380
+ - `has_key?` - **O(1)**(同上)
242
381
  - `min` / `max` - **O(1)**
243
382
  - `shift` / `pop` - O(log n)
244
383
  - `prev` / `succ` - O(log n)、O(1)ハッシュチェックと高速な走査開始により改善
384
+ - `clear` - **O(1)**(ノードは再利用されず GC に委ねられます)
245
385
 
246
386
  全要素のイテレーションはO(n)時間。
247
387
 
388
+ > **検索ミスについて:** キーの同一性を `eql?` ではなく順序で決めることの代償は、通常のキー型では
389
+ > ゼロです。20万件の Integer キーのツリーでの実測で、5万回の空振り検索が 8.1 ms(素のハッシュ探査のみの
390
+ > 場合は 6.3 ms)。差はクラス判定1回ぶんであって、木の降下ではありません。木を降りるのはクラス混在・
391
+ > 独自キーのツリーだけです(ミス1回あたり約 1.4 µs)。ヒット時・挿入・削除・イテレーション・範囲クエリには
392
+ > 影響ありません。
393
+
394
+
248
395
  ### RBTree vs Hash vs Array
249
396
 
250
397
  順序付き操作と空間的操作において、RBTreeは単に速いだけでなく、全く異なるクラスの性能を発揮。**50万件**でのベンチマーク:
data/README.md CHANGED
@@ -10,10 +10,10 @@ A pure Ruby implementation of the Red-Black Tree data structure, providing effic
10
10
  - **Ordered Operations**: Efficient sorted iteration, range queries (`lt`, `gt`, `between`), min/max retrieval.
11
11
  - **Multi-Value Support**: `MultiRBTree` class stores multiple values per key, with access to first or last value individually.
12
12
  - **Pure Ruby**: No C extensions required. Works on MRI, JRuby, TruffleRuby, and all Ruby implementations.
13
- - **Hybrid Indexing**: Internal hash index enables O(1) key lookup and membership checks — matching standard Hash performance.
13
+ - **Hybrid Indexing**: Internal hash index enables O(1) key lookup and membership checks — matching standard Hash performance — while ordering still decides key identity, so lookup and ordering never disagree (see [Key Requirements](#key-requirements)).
14
14
  - **Memory Efficiency**: Node recycling with auto-shrinking pool (`AutoShrinkNodePool`) drastically reduces GC pressure in long-running apps.
15
15
  - **Nearest Key Search**: Finds the closest numeric key in O(log n) time — ideal for spatial or temporal queries.
16
- - **Safe Iteration**: Use `safe: true` to safely modify the tree (insert/delete) during iteration.
16
+ - **Safe Iteration**: Use `safe: true` to safely modify the tree (insert/delete) during iteration. Without it, modifying during iteration raises `RuntimeError` rather than silently misbehaving.
17
17
 
18
18
  ## Installation
19
19
 
@@ -71,12 +71,16 @@ tree.each { |key, value| puts "#{key}: #{value}" }
71
71
  # Modification during iteration
72
72
  # Unlike standard Ruby Hash/Array, modification during iteration is fully supported
73
73
  # with the `safe: true` option. This allows you to delete or insert keys safely while iterating.
74
+ # Without it, modifying the tree mid-iteration raises RuntimeError (see "Iteration Safety").
74
75
  tree.each(safe: true) { |k, v| tree.delete(k) if k.even? }
75
76
  tree.each(reverse: true) { |k, v| puts k } # Same as reverse_each
76
77
 
77
- # Min and max
78
+ # Min and max — O(1), and Enumerable-compatible when given a count or a block
78
79
  tree.min # => [1, "one"]
79
80
  tree.max # => [20, "twenty"]
81
+ tree.first(2) # => [[1, "one"], [2, "two"]]
82
+ tree.last(2) # => [[10, "ten"], [20, "twenty"]]
83
+ tree.min(2) # => [[1, "one"], [2, "two"]]
80
84
 
81
85
  # Range queries (return Enumerator, use .to_a for Array)
82
86
  tree.lt(10).to_a # => [[1, "one"], [2, "two"], [3, "three"]]
@@ -194,13 +198,66 @@ tree = RBTree.new({1 => 'one', 2 => 'two'})
194
198
  # Convert to Array (via Enumerable)
195
199
  tree.to_a # => [[1, "one"], [2, "two"]]
196
200
 
197
- # Convert to Hash
201
+ # Convert to Hash (keys are inserted in ascending order)
198
202
  tree.to_h # => {1 => "one", 2 => "two"}
199
203
 
200
- # Merge another tree, hash, or enumerable
204
+ # MultiRBTree maps each key to a copy of its value list
205
+ multi = MultiRBTree.new
206
+ multi.insert(1, 'a'); multi.insert(1, 'b')
207
+ multi.to_h # => {1 => ["a", "b"]}
208
+
209
+ # Merge (destructive)
201
210
  other = {3 => 'three'}
202
211
  tree.merge!(other)
203
212
  tree.size # => 3
213
+
214
+ # Merge (non-destructive) — returns a new tree
215
+ merged = tree.merge({4 => 'four'})
216
+
217
+ # Merge with block for duplicate key resolution
218
+ merged = tree.merge({1 => 'ONE'}) { |key, old_val, new_val| old_val }
219
+
220
+ # Invert keys and values
221
+ tree = RBTree.new({1 => 'a', 2 => 'b', 3 => 'c'})
222
+ tree.invert.to_a # => [["a", 1], ["b", 2], ["c", 3]]
223
+ ```
224
+
225
+ ### Filtering and Copying
226
+
227
+ > **Note:** `dup`, `select`, `reject`, `delete_if`, `reject!`, `keep_if`, `invert`, and `merge` are convenience methods composed from existing primitives. They provide no speed advantage over manual composition — their value is in readability and API completeness.
228
+
229
+ Create copies or filter trees using familiar Ruby idioms:
230
+
231
+ ```ruby
232
+ tree = RBTree.new({1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'})
233
+
234
+ # Deep copy — independent of original
235
+ copy = tree.dup
236
+ copy.delete(1)
237
+ tree.size # => 4 (unchanged)
238
+
239
+ # select / reject — return a new tree
240
+ evens = tree.select { |k, _| k.even? } # => {2=>"two", 4=>"four"}
241
+ odds = tree.reject { |k, _| k.even? } # => {1=>"one", 3=>"three"}
242
+
243
+ # delete_if / keep_if — modify in place
244
+ tree.delete_if { |k, _| k > 2 }
245
+ tree.to_a # => [[1, "one"], [2, "two"]]
246
+
247
+ # reject! — like delete_if, but returns nil if nothing changed
248
+ tree.reject! { |_, _| false } # => nil
249
+ ```
250
+
251
+ For `MultiRBTree`, `delete_if` and `keep_if` operate at individual value granularity:
252
+
253
+ ```ruby
254
+ tree = MultiRBTree.new
255
+ tree.insert(1, 'a')
256
+ tree.insert(1, 'b')
257
+ tree.insert(2, 'c')
258
+
259
+ tree.delete_if { |k, v| k == 1 && v == 'a' }
260
+ tree.to_a # => [[1, "b"], [2, "c"]] — only 'a' was removed
204
261
  ```
205
262
 
206
263
  ### MultiRBTree Value Array Access
@@ -231,20 +288,112 @@ tree.min # => [1, "second"] (first value of min key)
231
288
  tree.max(last: true) # => [2, "b"] (last value of max key)
232
289
  ```
233
290
 
291
+ ## Semantics
292
+
293
+ ### Key Requirements
294
+
295
+ Keys must be mutually comparable with `<=>`. Ordering — not `eql?`/`hash` — decides key
296
+ identity: any key for which `(key <=> stored_key) == 0` refers to the same entry.
297
+
298
+ ```ruby
299
+ tree = RBTree.new
300
+ tree[1] = 'int'
301
+
302
+ tree[1.0] # => "int" (1.0 <=> 1 is 0, so it is the same key)
303
+ tree.has_key?(1.0) # => true
304
+ tree[1.0] = 'float'
305
+ tree.to_a # => [[1, "float"]] — the first key object inserted is kept
306
+ ```
307
+
308
+ The internal hash index is keyed by `eql?`/`hash`, so a lookup like `tree[1.0]` above
309
+ misses it and resolves through the tree instead. That fallback is skipped whenever it is
310
+ provably futile — when every stored key and the lookup key share one class for which
311
+ `<=>`-equality coincides with `eql?`, any matching key would already have been found in
312
+ the index. `Integer`, `Float`, `Rational`, `String`, `Symbol` and `Time` qualify, so:
313
+
314
+ | Tree | Lookup cost |
315
+ |---|---|
316
+ | Keys all of one core class above | **O(1)**, hits and misses alike |
317
+ | Mixed key classes, or a custom key class | O(1) on a hash hit, O(log n) otherwise |
318
+
319
+ A custom key class that defines `<=>` alongside a matching `eql?`/`hash` can opt into the
320
+ O(1) path:
321
+
322
+ ```ruby
323
+ class Version
324
+ include Comparable
325
+ attr_reader :parts
326
+ def initialize(parts) = @parts = parts
327
+ def <=>(other) = parts <=> other.parts
328
+ def eql?(other) = other.instance_of?(Version) && parts == other.parts
329
+ def hash = parts.hash
330
+ end
331
+
332
+ RBTree.coherent_key_class(Version)
333
+ ```
334
+
335
+ This is purely an optimization hint — without it such trees are still correct, just with
336
+ an O(log n) descent on lookups that miss the index. A class that defines `<=>` but leaves
337
+ `eql?`/`hash` at their default identity semantics must **not** be declared.
338
+
339
+ Keys that cannot be compared at all are reported as absent by lookups rather than raising:
340
+
341
+ ```ruby
342
+ tree['no <=> with Integer'] # => nil
343
+ tree.has_key?('...') # => false
344
+ tree['...'] = 'x' # raises ArgumentError — insertion needs an ordering
345
+ ```
346
+
347
+ ### Iteration Safety
348
+
349
+ Iteration walks live node links, and deleted nodes are recycled by the allocator, so
350
+ mutating the tree during a plain iteration is not merely lossy — it can yield keys out of
351
+ order. This is detected and reported:
352
+
353
+ ```ruby
354
+ tree.each { |k, v| tree.delete(k) }
355
+ # => RuntimeError: can't modify RBTree during iteration (pass `safe: true` to iterate safely)
356
+
357
+ tree.each(safe: true) { |k, v| tree.delete(k) } # supported
358
+ ```
359
+
360
+ The guard covers `each`, `reverse_each`, `keys`, all range queries, and any `Enumerator`
361
+ they return — including one held across a later mutation. Replacing the value of an
362
+ existing key is not a structural change and stays allowed. `delete_if`, `keep_if` and
363
+ `reject!` iterate safely on your behalf.
364
+
365
+ In `MultiRBTree`, `safe: true` snapshots each key's value list, so removing values while
366
+ that key is being iterated will not skip its siblings; values removed during the
367
+ iteration of their own key may still be yielded.
368
+
369
+ ### Thread Safety
370
+
371
+ No operation is synchronized. The tree, its hash index, and the node allocator are all
372
+ shared mutable state, so concurrent access from multiple threads requires external
373
+ locking. Note that a node allocator passed to several trees (including via `dup`) is
374
+ shared state between them.
375
+
234
376
  ## Performance
235
377
 
236
378
  All major operations run in **O(log n)** time:
237
379
 
238
380
  - `insert(key, value)` - O(log n)
239
381
  - `delete(key)` - O(log n)
240
- - `value(key)` / `[]` - **O(1)** (hybrid hash index)
241
- - `has_key?` - **O(1)** (hybrid hash index)
382
+ - `value(key)` / `[]` - **O(1)** (O(log n) only for a miss on a mixed-class or custom-key tree — see [Key Requirements](#key-requirements))
383
+ - `has_key?` - **O(1)** (same proviso)
242
384
  - `min` / `max` - **O(1)**
243
385
  - `shift` / `pop` - O(log n)
244
386
  - `prev` / `succ` - O(log n) with O(1) hash check and faster startup
387
+ - `clear` - **O(1)** (nodes are left to the GC rather than recycled)
245
388
 
246
389
  Iteration over all elements takes O(n) time.
247
390
 
391
+ > **On lookup misses:** resolving key identity through ordering rather than `eql?` costs
392
+ > nothing for ordinary key types. Measured on a 200,000-entry Integer-keyed tree, 50,000
393
+ > missing lookups take 8.1 ms against 6.3 ms for a bare hash probe — the difference is a
394
+ > class check, not a tree descent. Only a mixed-class or custom-key tree pays the descent
395
+ > (~1.4 µs per miss). Hits, insert, delete, iteration and range queries are unaffected.
396
+
248
397
  ### RBTree vs Hash vs Array (Overwhelming Power)
249
398
 
250
399
  For ordered and spatial operations, RBTree is not just faster—it is in a completely different class. The following benchmarks were conducted with **500,000 items**:
data/Rakefile CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rdoc/task"
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "lib" << "test"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ t.warning = false
11
+ end
5
12
 
6
13
  RDoc::Task.new(:rdoc) do |rdoc|
7
14
  rdoc.rdoc_dir = "doc"
@@ -2,5 +2,5 @@
2
2
 
3
3
  class RBTree
4
4
  # The version of the rbtree-ruby gem
5
- VERSION = "0.3.5"
5
+ VERSION = "0.4.0"
6
6
  end