├── .bnsignore
├── .gitignore
├── .ruby-gemset
├── .ruby-version
├── .travis.yml
├── Gemfile
├── Gemfile.lock
├── History.txt
├── README.md
├── Rakefile
├── lib
├── btree.rb
└── btree
│ ├── node.rb
│ └── tree.rb
├── test
└── test_btree.rb
└── version.txt
/.bnsignore:
--------------------------------------------------------------------------------
1 | # The list of files that should be ignored by Mr Bones.
2 | # Lines that start with '#' are comments.
3 | #
4 | # A .gitignore file can be used instead by setting it as the ignore
5 | # file in your Rakefile:
6 | #
7 | # Bones {
8 | # ignore_file '.gitignore'
9 | # }
10 | #
11 | # For a project with a C extension, the following would be a good set of
12 | # exclude patterns (uncomment them if you want to use them):
13 | # *.[oa]
14 | # *~
15 | announcement.txt
16 | coverage
17 | doc
18 | pkg
19 | *.swp
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /pkg/
2 | /doc/
3 | /vendor/
4 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | btree
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.2.0
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: ruby
3 | cache:
4 | directories:
5 | - node_modules
6 | notifications:
7 | email: false
8 | rvm:
9 | - 2.4.1
10 | branches:
11 | except:
12 | - "/^v\\d+\\.\\d+\\.\\d+$/"
13 | deploy:
14 | api_key:
15 | secure: pDZgw7xhVcsEHA9pNrww1hexa50UqZa5mn2JvnnSw6JejLGhPFP0V520/p5GrdW/wZpHuWSd5L5WuM1Uz6ft0Q+LOeIuHC80WHFNgY6E+D7jS06yX2pELh94lWMlFefZvZbT/qv0LQx8fdDh1Dou6abkXHWCApC6VMJ7vBhi8kw=
16 | provider: rubygems
17 | on:
18 | tags: true
19 | gem: btree
20 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | source "https://rubygems.org"
3 | #repo_name "seifertd/Ruby-BTree"
4 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
5 |
6 | # gem "rails"
7 |
8 | # Added at 2017-11-09 08:39:45 -0800 by doug:
9 | gem "rake", "~> 13", :group => [:development, :test]
10 |
11 | # Added at 2017-11-09 08:40:02 -0800 by doug:
12 | gem "bones", "~> 3.9", :group => [:development, :test]
13 |
14 | gem "minitest", :group => [:development, :test]
15 | # Added at 2017-11-09 08:40:12 -0800 by doug:
16 | gem "shoulda", "~> 3.5", :group => [:development, :test]
17 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (7.0.8)
5 | concurrent-ruby (~> 1.0, >= 1.0.2)
6 | i18n (>= 1.6, < 2)
7 | minitest (>= 5.1)
8 | tzinfo (~> 2.0)
9 | bones (3.9.0)
10 | little-plugger (~> 1.1)
11 | loquacious (~> 1.9)
12 | rake (~> 13.0)
13 | rdoc (~> 6.0)
14 | concurrent-ruby (1.2.2)
15 | i18n (1.14.1)
16 | concurrent-ruby (~> 1.0)
17 | little-plugger (1.1.4)
18 | loquacious (1.9.1)
19 | minitest (5.20.0)
20 | psych (5.1.2)
21 | stringio
22 | rake (13.0.6)
23 | rdoc (6.6.3.1)
24 | psych (>= 4.0.0)
25 | shoulda (3.6.0)
26 | shoulda-context (~> 1.0, >= 1.0.1)
27 | shoulda-matchers (~> 3.0)
28 | shoulda-context (1.2.2)
29 | shoulda-matchers (3.1.3)
30 | activesupport (>= 4.0.0)
31 | stringio (3.1.0)
32 | tzinfo (2.0.6)
33 | concurrent-ruby (~> 1.0)
34 |
35 | PLATFORMS
36 | x86_64-linux
37 |
38 | DEPENDENCIES
39 | bones (~> 3.9)
40 | minitest
41 | rake (~> 13)
42 | shoulda (~> 3.5)
43 |
44 | BUNDLED WITH
45 | 2.4.12
46 |
--------------------------------------------------------------------------------
/History.txt:
--------------------------------------------------------------------------------
1 | == 0.0.0 / 2010-12-20
2 |
3 | * 1 major enhancement
4 | * Initial Release!
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | btree
2 | =====
3 |
4 | Pure ruby implementation of a btree as described in Introduction to Algorithms by
5 | Cormen, Leiserson, Rivest and Stein, Chapter 18.
6 |
7 | Features
8 | --------
9 |
10 | * Create B-Tree of arbitrary degree (2 by default)
11 | * Insert key value pairs using a Map like interface
12 | * Query for values associated with keys using a Map like interface
13 | * Keys can be any object supporting comparison operators: <, > and ==
14 |
15 | Examples
16 | --------
17 |
18 | require 'btree'
19 | tree = Btree.create # default degree = 2
20 | tree = Btree.create(5) # degree = 5
21 | tree['foo'] = 'foo value'
22 | tree['bar'] = 'bar value'
23 |
24 | puts "key 'foo' has value: #{tree['foo']}"
25 | puts "BTree has #{tree.size} key-value pairs"
26 |
27 | Future
28 | ------
29 |
30 | * Deletion is not implemented. TODO: Implement deletion
31 | * Attempt to insert existing key raises RuntimeError.
32 | TODO: Allow replacement of value associated with existing key instead of raising RuntimeError.
33 | * Persist state to backing store
34 |
35 | Install
36 | -------
37 |
38 | * gem install btree
39 |
40 | Author
41 | ------
42 |
43 | Original author: Douglas A. Seifert (doug@dseifert.net)
44 |
45 | License
46 | -------
47 |
48 | (The MIT License)
49 |
50 | Copyright (c) 2010,2011 Douglas A. Seifert
51 |
52 | Permission is hereby granted, free of charge, to any person obtaining
53 | a copy of this software and associated documentation files (the
54 | 'Software'), to deal in the Software without restriction, including
55 | without limitation the rights to use, copy, modify, merge, publish,
56 | distribute, sublicense, and/or sell copies of the Software, and to
57 | permit persons to whom the Software is furnished to do so, subject to
58 | the following conditions:
59 |
60 | The above copyright notice and this permission notice shall be
61 | included in all copies or substantial portions of the Software.
62 |
63 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
64 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
65 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
66 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
67 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
68 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
69 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
70 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 |
2 | begin
3 | require 'bones'
4 | rescue LoadError
5 | abort '### Please install the "bones" gem ###'
6 | end
7 |
8 | task :default => 'test:run'
9 | task 'gem:release' => 'test:run'
10 |
11 | Bones {
12 | name 'btree'
13 | authors 'Douglas A. Seifert'
14 | email 'doug@dseifert.net'
15 | url 'https://github.com/seifertd/Ruby-BTree'
16 | readme_file 'README.md'
17 | exclude ['.bnsignore', '.gitignore', '.ruby-gemset', '.ruby-version', '.travis.yml', 'vendor', '.git']
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/lib/btree.rb:
--------------------------------------------------------------------------------
1 | # :main: README.md
2 | module Btree
3 |
4 | # :stopdoc:
5 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
6 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
7 | # :startdoc:
8 |
9 | # Returns the version string for the library.
10 | #
11 | def self.version
12 | @version ||= File.read(path('version.txt')).strip
13 | end
14 |
15 | # Returns the library path for the module. If any arguments are given,
16 | # they will be joined to the end of the libray path using
17 | # File.join.
18 | #
19 | def self.libpath( *args, &block )
20 | rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
21 | if block
22 | begin
23 | $LOAD_PATH.unshift LIBPATH
24 | rv = block.call
25 | ensure
26 | $LOAD_PATH.shift
27 | end
28 | end
29 | return rv
30 | end
31 |
32 | # Returns the lpath for the module. If any arguments are given,
33 | # they will be joined to the end of the path using
34 | # File.join.
35 | #
36 | def self.path( *args, &block )
37 | rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
38 | if block
39 | begin
40 | $LOAD_PATH.unshift PATH
41 | rv = block.call
42 | ensure
43 | $LOAD_PATH.shift
44 | end
45 | end
46 | return rv
47 | end
48 |
49 | # Utility method used to require all files ending in .rb that lie in the
50 | # directory below this file that has the same name as the filename passed
51 | # in. Optionally, a specific _directory_ name can be passed in such that
52 | # the _filename_ does not have to be equivalent to the directory.
53 | #
54 | def self.require_all_libs_relative_to( fname, dir = nil )
55 | dir ||= ::File.basename(fname, '.*')
56 | search_me = ::File.expand_path(
57 | ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
58 |
59 | Dir.glob(search_me).sort.each {|rb| require rb}
60 | end
61 |
62 | def self.create(degree)
63 | raise "Degree of Btree must be >= 2" if degree < 2
64 | return Btree::Tree.new(degree)
65 | end
66 |
67 | end # module Btree
68 |
69 | Btree.require_all_libs_relative_to(__FILE__)
70 |
71 |
--------------------------------------------------------------------------------
/lib/btree/node.rb:
--------------------------------------------------------------------------------
1 | class Btree::Node
2 | def initialize(degree)
3 | @degree = degree
4 | @keys = []
5 | @children = []
6 | end
7 |
8 | def dump(level = 0)
9 | @keys.each_with_index do |key, idx|
10 | puts "LEVEL: #{level} => #{key.first}: full? #{full?} leaf? #{leaf?} children: #{values.inspect}"
11 | if @children[idx]
12 | @children[idx].dump(level + 1)
13 | end
14 | end
15 | (@children[@keys.size..-1] || []).each do |c|
16 | c.dump(level+1)
17 | end
18 | nil
19 | end
20 |
21 | def add_child(node)
22 | @children << node
23 | end
24 |
25 | def children
26 | @children.dup.freeze
27 | end
28 |
29 | def keys
30 | @keys.map(&:first).freeze
31 | end
32 |
33 | def values
34 | @keys.map(&:last).freeze
35 | end
36 |
37 | def full?
38 | size >= 2 * @degree - 1
39 | end
40 |
41 | def leaf?
42 | @children.length == 0
43 | end
44 |
45 | def size
46 | @keys.size
47 | end
48 |
49 | def values_of(range)
50 |
51 | result = Array.new
52 |
53 | i = 1
54 | while i <= size && range.end >= @keys[i-1].first
55 | if range.cover? @keys[i-1].first
56 | result << @keys[i-1].last
57 | child = @children[i-1].values_of(range) unless leaf?
58 | result += child if child
59 | end
60 | i += 1
61 | end
62 |
63 | result
64 |
65 | end
66 |
67 |
68 | def value_of(key)
69 |
70 | return values_of(key) if key.kind_of? Range
71 |
72 | i = 1
73 | while i <= size && key > @keys[i-1].first
74 | i += 1
75 | end
76 |
77 | #puts "Getting value of key #{key}, i = #{i}, keys = #{@keys.inspect}, leaf? #{leaf?}, numchildren: #{@children.size}"
78 |
79 | if i <= size && key == @keys[i-1].first
80 | #puts "Found key: #{key.inspect}"
81 | return @keys[i-1].last
82 | elsif leaf?
83 | #puts "We are a leaf, no more children, so val is nil"
84 | return nil
85 | else
86 | #puts "Looking into child #{i}"
87 | return @children[i-1].value_of(key)
88 | end
89 | end
90 |
91 | def insert(key, value)
92 | i = size - 1
93 | #puts "INSERTING #{key} INTO NODE: #{self.inspect}"
94 | if leaf?
95 | raise "Duplicate key" if @keys.any?{|(k,v)| k == key } #OPTIMIZE: This is inefficient
96 | while i >= 0 && @keys[i] && key < @keys[i].first
97 | @keys[i+1] = @keys[i]
98 | i -= 1
99 | end
100 | @keys[i+1] = [key, value]
101 | else
102 | while i >= 0 && @keys[i] && key < @keys[i].first
103 | i -= 1
104 | end
105 | #puts " -- INSERT KEY INDEX #{i}"
106 | if @children[i+1] && @children[i+1].full?
107 | split(i+1)
108 | if key > @keys[i+1].first
109 | i += 1
110 | end
111 | end
112 | @children[i+1].insert(key, value)
113 | end
114 | end
115 |
116 | def split(child_idx)
117 | raise "Invalid child index #{child_idx} in split, num_children = #{@children.size}" if child_idx < 0 || child_idx >= @children.size
118 | #puts "SPLIT1: #{self.inspect}"
119 | splitee = @children[child_idx]
120 | y = Btree::Node.new(@degree)
121 | z = Btree::Node.new(@degree)
122 | (@degree-1).times do |j|
123 | z._keys[j] = splitee._keys[j+@degree]
124 | y._keys[j] = splitee._keys[j]
125 | end
126 | if !splitee.leaf?
127 | @degree.times do |j|
128 | z._children[j] = splitee._children[j+@degree]
129 | y._children[j] = splitee._children[j]
130 | end
131 | end
132 | mid_val = splitee._keys[@degree-1]
133 | #puts "SPLIT2: #{self.inspect}"
134 | (@keys.size).downto(child_idx) do |j|
135 | @children[j+1] = @children[j]
136 | end
137 |
138 | @children[child_idx+1] = z
139 | @children[child_idx] = y
140 |
141 | #puts "SPLIT3: #{self.inspect}"
142 |
143 | (@keys.size - 1).downto(child_idx) do |j|
144 | @keys[j+1] = @keys[j]
145 | end
146 |
147 | #puts "SPLIT4: #{self.inspect}"
148 |
149 | @keys[child_idx] = mid_val
150 | #puts "SPLIT5: #{self.inspect}"
151 | end
152 |
153 | protected
154 |
155 | def _keys
156 | @keys
157 | end
158 |
159 | def _children
160 | @children
161 | end
162 |
163 | end
164 |
--------------------------------------------------------------------------------
/lib/btree/tree.rb:
--------------------------------------------------------------------------------
1 | class Btree::Tree
2 | attr_reader :root, :degree, :size
3 |
4 | # Creates a BTree of degree 2 by default. Keys
5 | # Must support being compared using >, < and == methods.
6 | def initialize(degree = 2)
7 | @degree = degree
8 | @root = Btree::Node.new(@degree)
9 | @size = 0
10 | end
11 |
12 | # Insert a key-value pair into the btree
13 | def insert(key, value = nil)
14 | node = @root
15 | if node.full?
16 | @root = Btree::Node.new(@degree)
17 | @root.add_child(node)
18 | @root.split(@root.children.size - 1)
19 | #puts "After split, root = #{@root.inspect}"
20 | # split child(@root, 1)
21 | node = @root
22 | end
23 | node.insert(key, value)
24 | @size += 1
25 | return self
26 | end
27 |
28 | # puts internal state
29 | def dump
30 | @root.dump
31 | end
32 |
33 | # Get value associated with the specified key
34 | def value_of(key)
35 | @root.value_of(key)
36 | end
37 |
38 | # Support map like access
39 | alias_method :"[]", :value_of
40 |
41 | # Support map like key-value setting
42 | alias_method :"[]=", :insert
43 |
44 | end
45 |
--------------------------------------------------------------------------------
/test/test_btree.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'btree'
3 | require 'shoulda'
4 |
5 | class TestBtree < Minitest::Test
6 | def test_insert_notfull
7 | t = Btree.create(5)
8 | t.insert(5, "5")
9 | assert_equal 1, t.root.size
10 | assert !t.root.full?
11 | assert_equal 5, t.degree
12 | assert_equal 1, t.size
13 | end
14 |
15 | def test_degree_too_small
16 | assert_raises(RuntimeError) do
17 | Btree.create(1)
18 | end
19 | end
20 |
21 | def test_insert_duplicate_key
22 | t = Btree.create(2)
23 | t.insert(1, "1")
24 | assert_raises(RuntimeError) do
25 | t.insert(1, "1")
26 | end
27 | end
28 |
29 | def test_insert_a_lot
30 | t = Btree.create(2)
31 | 45.times do |i|
32 | t.insert i, i*i
33 | end
34 | assert_equal 45, t.size
35 | 45.times do |i|
36 | assert_equal i*i, t.value_of(i)
37 | end
38 | end
39 |
40 | def test_value_of
41 | t = Btree.create(5)
42 | t.insert(1, "foo")
43 | t.insert(5, "bar")
44 | t.insert(7, "baz")
45 | t.insert(3, "findme")
46 | assert_equal "findme", t.value_of(3)
47 | assert_equal "baz", t.value_of(7)
48 | assert_equal "bar", t.value_of(5)
49 | assert_equal "foo", t.value_of(1)
50 | assert_nil t.value_of(11)
51 | assert_equal 4, t.size
52 | assert_equal ["findme", "bar"], t.value_of(3..6)
53 | end
54 |
55 | def test_fill_root
56 | t = Btree.create(2)
57 | 3.times {|n| t.insert(n, n.to_s)}
58 | assert t.root.full?
59 | assert_equal [0,1,2], t.root.keys
60 | assert_equal ['0','1','2'], t.root.values
61 | assert_equal 3, t.size
62 | end
63 |
64 | context 'full root' do
65 | setup do
66 | @t = Btree.create(2)
67 | 3.times {|n| @t.insert(n*2, n.to_s)}
68 | end
69 | should "be able to insert at end" do
70 | @t.insert(10, "10")
71 | assert_equal "10", @t.value_of(10)
72 | assert_equal "0", @t.value_of(0)
73 | assert_equal "1", @t.value_of(2)
74 | assert_equal "2", @t.value_of(4)
75 | assert_equal 4, @t.size
76 | end
77 | should "be able to insert at front" do
78 | @t.insert(-1, "10")
79 | assert_equal "10", @t.value_of(-1)
80 | assert_equal "0", @t.value_of(0)
81 | assert_equal "1", @t.value_of(2)
82 | assert_equal "2", @t.value_of(4)
83 | assert_equal 4, @t.size
84 | end
85 | should "be able to insert in the middle" do
86 | @t.insert(3, "10")
87 | assert_equal "10", @t.value_of(3)
88 | assert_equal "0", @t.value_of(0)
89 | assert_equal "1", @t.value_of(2)
90 | assert_equal "2", @t.value_of(4)
91 | assert_equal 4, @t.size
92 | end
93 | context 'full last child' do
94 | setup do
95 | 2.times {|n| @t.insert(n*2 + 10, "foo") }
96 | end
97 | should "have full last child" do
98 | assert @t.root.children.last.full?, "Last child of root should be full"
99 | assert_equal 5, @t.size
100 | end
101 | should "be able to add to end" do
102 | @t.insert(100, "YEAH!")
103 | assert_equal "YEAH!", @t.value_of(100)
104 | assert_equal 6, @t.size
105 | end
106 | should "be able to insert many" do
107 | 10.times { |n| @t.insert((n+1) * 100, "YEAH!") }
108 | assert_equal "YEAH!", @t.value_of(100)
109 | assert_equal 15, @t.size
110 | end
111 | end
112 | context 'second split of root' do
113 | setup do
114 | 6.times {|n| @t.insert(n*2 + 10, "foo") }
115 | end
116 | should "work" do
117 | assert_equal 'foo', @t.value_of(10)
118 | assert_equal 9, @t.size
119 | end
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | 1.0.0
2 |
--------------------------------------------------------------------------------