Skip to content

Commit 8aaeb4d

Browse files
committed
Add << support for trie, AA tree, and B-tree
Override TrieNode#<< to insert words via insert(word) semantics.\n\nAdd << shorthand to AATree and BTree for [key, value], { key:, value: }, and Entry inputs, with tests in both test-unit and RSpec suites.\n\nUse add() during JSON reconstruction to avoid coupling structural rebuilds to << overrides, and document that FenwickTree and SegmentTree intentionally do not support <<.\n\nUpdate CHANGELOG with the operator behavior changes.
1 parent 344c937 commit 8aaeb4d

13 files changed

Lines changed: 211 additions & 1 deletion

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Changes section to scan for breaking or behavioral changes.
66

77
## Release History
88

9+
### 3.0.0pre / 2026-02-10
10+
11+
* Make `Tree::TrieNode#<<` use trie word-insert semantics (`insert(word)`)
12+
instead of generic child-node attachment semantics.
13+
* Add `<<` insertion shorthand to `Tree::AATree` and `Tree::BTree` for
14+
`[key, value]`, `{ key:, value: }`, and `Entry` inputs.
15+
916
### 3.0.0pre / 2026-02-09
1017

1118
* Add runnable example scripts for each supported tree type under `examples/`

lib/tree/aatree.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ def insert(key, value = nil)
8787
inserted
8888
end
8989

90+
# Insert an entry using natural +<<+ syntax.
91+
#
92+
# @param [Array, Hash, Tree::AATree::Entry] entry Input entry as
93+
# +[key, value]+, +{ key:, value: }+, or {Tree::AATree::Entry}.
94+
# @return [Tree::AATree::Entry] The inserted entry.
95+
#
96+
# @raise [ArgumentError] If the input format is unsupported.
97+
def <<(entry)
98+
key, value = entry_to_pair(entry)
99+
insert(key, value)
100+
end
101+
90102
# Search for a key in the AA tree.
91103
#
92104
# @param [Object] key The key to search for.
@@ -271,6 +283,48 @@ def <=>(other)
271283

272284
private
273285

286+
# Convert a supported entry input into a key/value pair.
287+
#
288+
# @param [Array, Hash, Tree::AATree::Entry] entry Input entry.
289+
# @return [Array<Object, Object>] The +[key, value]+ pair.
290+
#
291+
# @raise [ArgumentError] If the input format is unsupported.
292+
def entry_to_pair(entry)
293+
return [entry.key, entry.value] if entry.is_a?(Entry)
294+
return array_entry_to_pair(entry) if entry.is_a?(Array)
295+
return hash_entry_to_pair(entry) if entry.is_a?(Hash)
296+
297+
raise ArgumentError, 'AA tree << expects [key, value], { key:, value: }, or Entry.'
298+
end
299+
300+
# Convert an array entry into a key/value pair.
301+
#
302+
# @param [Array] entry Input as +[key, value]+.
303+
# @return [Array<Object, Object>] The +[key, value]+ pair.
304+
#
305+
# @raise [ArgumentError] If the array shape is invalid.
306+
def array_entry_to_pair(entry)
307+
raise ArgumentError, 'AA tree << expects [key, value].' unless entry.length == 2
308+
309+
[entry[0], entry[1]]
310+
end
311+
312+
# Convert a hash entry into a key/value pair.
313+
#
314+
# @param [Hash] entry Input as +{ key:, value: }+.
315+
# @return [Array<Object, Object>] The +[key, value]+ pair.
316+
#
317+
# @raise [ArgumentError] If the hash shape is invalid.
318+
def hash_entry_to_pair(entry)
319+
has_symbol_key = entry.key?(:key)
320+
has_string_key = entry.key?('key')
321+
raise ArgumentError, 'AA tree << expects { key:, value: }.' unless has_symbol_key || has_string_key
322+
323+
key = has_symbol_key ? entry[:key] : entry['key']
324+
value = entry.key?(:value) ? entry[:value] : entry['value']
325+
[key, value]
326+
end
327+
274328
# Insert an entry into a node and return the new subtree root.
275329
#
276330
# @param [Tree::AATree::Node, nil] node The current subtree root.

lib/tree/btree.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ def insert(key, value = nil)
152152
insert_non_full(root, Entry.new(key, value))
153153
end
154154

155+
# Insert an entry using natural +<<+ syntax.
156+
#
157+
# @param [Array, Hash, Tree::BTree::Entry] entry Input entry as
158+
# +[key, value]+, +{ key:, value: }+, or {Tree::BTree::Entry}.
159+
# @return [Tree::BTree::Entry] The inserted entry.
160+
#
161+
# @raise [ArgumentError] If the input format is unsupported.
162+
def <<(entry)
163+
key, value = entry_to_pair(entry)
164+
insert(key, value)
165+
end
166+
155167
# Search for a key in the B-tree.
156168
#
157169
# @param [Object] key The key to search for.
@@ -351,6 +363,48 @@ def <=>(other)
351363

352364
private
353365

366+
# Convert a supported entry input into a key/value pair.
367+
#
368+
# @param [Array, Hash, Tree::BTree::Entry] entry Input entry.
369+
# @return [Array<Object, Object>] The +[key, value]+ pair.
370+
#
371+
# @raise [ArgumentError] If the input format is unsupported.
372+
def entry_to_pair(entry)
373+
return [entry.key, entry.value] if entry.is_a?(Entry)
374+
return array_entry_to_pair(entry) if entry.is_a?(Array)
375+
return hash_entry_to_pair(entry) if entry.is_a?(Hash)
376+
377+
raise ArgumentError, 'B-tree << expects [key, value], { key:, value: }, or Entry.'
378+
end
379+
380+
# Convert an array entry into a key/value pair.
381+
#
382+
# @param [Array] entry Input as +[key, value]+.
383+
# @return [Array<Object, Object>] The +[key, value]+ pair.
384+
#
385+
# @raise [ArgumentError] If the array shape is invalid.
386+
def array_entry_to_pair(entry)
387+
raise ArgumentError, 'B-tree << expects [key, value].' unless entry.length == 2
388+
389+
[entry[0], entry[1]]
390+
end
391+
392+
# Convert a hash entry into a key/value pair.
393+
#
394+
# @param [Hash] entry Input as +{ key:, value: }+.
395+
# @return [Array<Object, Object>] The +[key, value]+ pair.
396+
#
397+
# @raise [ArgumentError] If the hash shape is invalid.
398+
def hash_entry_to_pair(entry)
399+
has_symbol_key = entry.key?(:key)
400+
has_string_key = entry.key?('key')
401+
raise ArgumentError, 'B-tree << expects { key:, value: }.' unless has_symbol_key || has_string_key
402+
403+
key = has_symbol_key ? entry[:key] : entry['key']
404+
value = entry.key?(:value) ? entry[:value] : entry['value']
405+
[key, value]
406+
end
407+
354408
# Maximum entries per node.
355409
#
356410
# @return [Integer] The maximum entry count.

lib/tree/fenwicktree.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ module Tree
5656
#
5757
# Indices are zero-based for public methods.
5858
#
59+
# @note The +<<+ operator is intentionally not supported for this class.
60+
# Fenwick trees require explicit indexed updates via {#update} or {#[]=},
61+
# and do not model append-style insertion semantics.
62+
#
5963
class FenwickTree
6064
include Enumerable
6165
include Comparable

lib/tree/segmenttree.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ module Tree
5454
#
5555
# Indices are zero-based for public methods.
5656
#
57+
# @note The +<<+ operator is intentionally not supported for this class.
58+
# Segment trees require explicit indexed updates via {#update} or {#[]=},
59+
# and do not model append-style insertion semantics.
60+
#
5761
class SegmentTree
5862
include Enumerable
5963
include Comparable

lib/tree/trie.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,21 @@ def insert(word)
9797
current
9898
end
9999

100+
# Insert a word into the trie using natural +<<+ syntax.
101+
#
102+
# This overrides {Tree::TreeNode#<<}. For trie nodes, +<<+ inserts a word
103+
# (string) instead of attaching a child node directly.
104+
#
105+
# @param [String] word The word to insert.
106+
# @return [Tree::TrieNode] The terminal node for the inserted word.
107+
#
108+
# @raise [ArgumentError] If the word is empty or not a string.
109+
#
110+
# @see #insert
111+
def <<(word)
112+
insert(word)
113+
end
114+
100115
# Returns +true+ if the trie includes the specified word.
101116
#
102117
# @param [String] word The word to look up.

lib/tree/utils/json_converter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def json_create(json_hash)
125125
json_hash['children']&.each do |child|
126126
next unless child
127127

128-
node << child
128+
node.add(child)
129129
end
130130

131131
node

spec/tree/aa_tree_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,20 @@ def build_tree(entries)
155155
expect { tree.insert(nil, 'a') }.to raise_error(ArgumentError)
156156
end
157157
end
158+
159+
describe '<<' do
160+
it 'returns the inserted entry' do
161+
tree = build_tree([])
162+
inserted = tree << [10, 'a']
163+
164+
expect(inserted.key).to eq(10)
165+
end
166+
167+
it 'stores the inserted key/value pair' do
168+
tree = build_tree([])
169+
tree << [10, 'a']
170+
171+
expect(tree.search(10)).to eq('a')
172+
end
173+
end
158174
end

spec/tree/b_tree_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,20 @@ def inorder_keys(node, result = [])
207207
expect(tree <=> 123).to be_nil
208208
end
209209
end
210+
211+
describe '<<' do
212+
it 'returns the inserted entry' do
213+
tree = described_class.new(2)
214+
inserted = tree << [10, 'a']
215+
216+
expect(inserted.key).to eq(10)
217+
end
218+
219+
it 'stores the inserted key/value pair' do
220+
tree = described_class.new(2)
221+
tree << [10, 'a']
222+
223+
expect(tree.search(10)).to eq('a')
224+
end
225+
end
210226
end

spec/tree/trie_node_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,20 @@ def build_trie(words)
8080
expect(root.words_with_prefix('do')).to eq(%w[dog])
8181
end
8282
end
83+
84+
describe '<<' do
85+
it 'inserts a word' do
86+
root = described_class.new('')
87+
root << 'cat'
88+
89+
expect(root.include?('cat')).to be(true)
90+
end
91+
92+
it 'returns the terminal node for the inserted word' do
93+
root = described_class.new('')
94+
terminal = root << 'cat'
95+
96+
expect(terminal.terminal?).to be(true)
97+
end
98+
end
8399
end

0 commit comments

Comments
 (0)