btree 1.0.0 → 1.0.1
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 +5 -5
- data/Gemfile +12 -12
- data/Gemfile.lock +26 -31
- data/History.txt +19 -0
- data/Rakefile +171 -13
- data/btree.gemspec +44 -0
- data/lib/btree/node.rb +66 -38
- data/test/test_btree.rb +44 -2
- data/version.txt +1 -1
- metadata +20 -32
- data/lib/insert_alot.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5a5640c8a1cddfdfb4f56acbc605aaa5722ab154605f133a8dfa8b089cc32e1c
|
|
4
|
+
data.tar.gz: edf0f1348d367ddacb3337d42df137201064c981b5becc84ab6a0b1368990217
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7ded3fe792d31605f5a115402419f819c0bebfc72d38ac50fac86300bd595b871be8d73a19d538a75445a76579b327bdc2884f38e5606f94ca8761df976a26d
|
|
7
|
+
data.tar.gz: 0cee704dabf324cddfab2e6edac454fe7698cfdd8b936f013969aec0b4b22e75ef6ac2ca272ab2d04313b0bdd5b1b6d35ce0d4e54f5eb6d6789988c95d64121e
|
data/Gemfile
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
source "https://rubygems.org"
|
|
3
|
-
#repo_name "seifertd/Ruby-BTree"
|
|
4
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
|
5
3
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
gem "
|
|
4
|
+
# Runtime dependencies come from btree.gemspec, which declares none.
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
# shoulda-context supplies the context/should DSL the tests use. The shoulda
|
|
8
|
+
# meta-gem would also pull in shoulda-matchers, whose only purpose is Rails
|
|
9
|
+
# model assertions this project has no use for, and which drags in the whole
|
|
10
|
+
# activesupport tree behind it.
|
|
11
|
+
group :development, :test do
|
|
12
|
+
gem "rake", "~> 13"
|
|
13
|
+
gem "minitest", "~> 6.0"
|
|
14
|
+
gem "shoulda-context", "~> 2.0"
|
|
15
|
+
end
|
data/Gemfile.lock
CHANGED
|
@@ -1,41 +1,36 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
btree (1.0.1)
|
|
5
|
+
|
|
1
6
|
GEM
|
|
2
7
|
remote: https://rubygems.org/
|
|
3
8
|
specs:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
loquacious (~> 1.9)
|
|
12
|
-
rake (~> 12.0)
|
|
13
|
-
rdoc (~> 5.0)
|
|
14
|
-
concurrent-ruby (1.0.5)
|
|
15
|
-
i18n (0.9.1)
|
|
16
|
-
concurrent-ruby (~> 1.0)
|
|
17
|
-
little-plugger (1.1.4)
|
|
18
|
-
loquacious (1.9.1)
|
|
19
|
-
minitest (5.10.3)
|
|
20
|
-
rake (12.2.1)
|
|
21
|
-
rdoc (5.1.0)
|
|
22
|
-
shoulda (3.5.0)
|
|
23
|
-
shoulda-context (~> 1.0, >= 1.0.1)
|
|
24
|
-
shoulda-matchers (>= 1.4.1, < 3.0)
|
|
25
|
-
shoulda-context (1.2.2)
|
|
26
|
-
shoulda-matchers (2.8.0)
|
|
27
|
-
activesupport (>= 3.0.0)
|
|
28
|
-
thread_safe (0.3.6)
|
|
29
|
-
tzinfo (1.2.4)
|
|
30
|
-
thread_safe (~> 0.1)
|
|
9
|
+
drb (2.2.3)
|
|
10
|
+
minitest (6.0.6)
|
|
11
|
+
drb (~> 2.0)
|
|
12
|
+
prism (~> 1.5)
|
|
13
|
+
prism (1.9.0)
|
|
14
|
+
rake (13.4.2)
|
|
15
|
+
shoulda-context (2.0.0)
|
|
31
16
|
|
|
32
17
|
PLATFORMS
|
|
33
18
|
ruby
|
|
19
|
+
x86_64-linux
|
|
34
20
|
|
|
35
21
|
DEPENDENCIES
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
btree!
|
|
23
|
+
minitest (~> 6.0)
|
|
24
|
+
rake (~> 13)
|
|
25
|
+
shoulda-context (~> 2.0)
|
|
26
|
+
|
|
27
|
+
CHECKSUMS
|
|
28
|
+
btree (1.0.1)
|
|
29
|
+
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
30
|
+
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
31
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
32
|
+
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
33
|
+
shoulda-context (2.0.0) sha256=7adf45342cd800f507d2a053658cb1cce2884b616b26004d39684b912ea32c34
|
|
39
34
|
|
|
40
35
|
BUNDLED WITH
|
|
41
|
-
|
|
36
|
+
4.0.17
|
data/History.txt
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
== 1.0.1 / 2026-07-23
|
|
2
|
+
|
|
3
|
+
* 2 bug fixes
|
|
4
|
+
* Range queries now visit every subtree that can contain an in-range key.
|
|
5
|
+
Previously the descent stopped at the first key at or above the range end,
|
|
6
|
+
so values held in later subtrees were silently omitted from the result.
|
|
7
|
+
* Inserting a key that a node split had promoted into an internal node now
|
|
8
|
+
raises "Duplicate key". Previously the duplicate check ran only at the
|
|
9
|
+
leaf, so such a key was descended past and inserted a second time, leaving
|
|
10
|
+
one copy unreachable in a subtree.
|
|
11
|
+
|
|
12
|
+
* 1 minor enhancement
|
|
13
|
+
* Nodes locate keys by a hybrid linear/binary search, scanning small nodes
|
|
14
|
+
and using bsearch on large ones.
|
|
15
|
+
|
|
16
|
+
== 1.0.0 / 2017-11-09
|
|
17
|
+
|
|
18
|
+
* Initial gem release.
|
|
19
|
+
|
|
1
20
|
== 0.0.0 / 2010-12-20
|
|
2
21
|
|
|
3
22
|
* 1 major enhancement
|
data/Rakefile
CHANGED
|
@@ -1,19 +1,177 @@
|
|
|
1
|
+
require 'rake/testtask'
|
|
2
|
+
require 'rake/clean'
|
|
3
|
+
require 'rubygems/package_task'
|
|
1
4
|
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
SPEC = Gem::Specification.load('btree.gemspec')
|
|
6
|
+
VERSION = SPEC.version.to_s
|
|
7
|
+
GEM_FILE = "pkg/#{SPEC.full_name}.gem"
|
|
8
|
+
|
|
9
|
+
CLOBBER.include('pkg', 'doc')
|
|
10
|
+
|
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
|
12
|
+
t.libs << 'lib' << 'test'
|
|
13
|
+
t.test_files = FileList['test/test_*.rb']
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task :default => :test
|
|
17
|
+
|
|
18
|
+
Gem::PackageTask.new(SPEC) do |pkg|
|
|
19
|
+
pkg.need_zip = false
|
|
20
|
+
pkg.need_tar = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc 'Build the gem into pkg/'
|
|
24
|
+
task :build => :package
|
|
25
|
+
|
|
26
|
+
desc 'Install the built gem locally'
|
|
27
|
+
task :install => :build do
|
|
28
|
+
sh 'gem', 'install', GEM_FILE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Shells out with Bundler's environment stripped. Anything that inspects the
|
|
32
|
+
# gem as an end user would -- installing it, requiring it -- has to run outside
|
|
33
|
+
# this project's bundle, or it picks up the working tree instead of the package.
|
|
34
|
+
def unbundled(*cmd)
|
|
35
|
+
require 'bundler'
|
|
36
|
+
Bundler.with_unbundled_env { sh(*cmd) }
|
|
4
37
|
rescue LoadError
|
|
5
|
-
|
|
38
|
+
sh(*cmd)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rubygems_versions
|
|
42
|
+
require 'open-uri'
|
|
43
|
+
require 'json'
|
|
44
|
+
JSON.parse(URI.open("https://rubygems.org/api/v1/versions/#{SPEC.name}.json").read)
|
|
45
|
+
rescue OpenURI::HTTPError, SocketError, JSON::ParserError => e
|
|
46
|
+
warn "WARNING: could not reach rubygems.org (#{e.class}: #{e.message})."
|
|
47
|
+
nil
|
|
6
48
|
end
|
|
7
49
|
|
|
8
|
-
|
|
9
|
-
|
|
50
|
+
namespace :release do
|
|
51
|
+
desc 'Check the working tree, metadata and credentials are fit to release'
|
|
52
|
+
task :check do
|
|
53
|
+
problems = []
|
|
54
|
+
|
|
55
|
+
problems << 'working tree is dirty; commit or stash first' unless
|
|
56
|
+
`git status --porcelain`.strip.empty?
|
|
57
|
+
|
|
58
|
+
# The tag has to point at a commit that exists somewhere other than this
|
|
59
|
+
# laptop, otherwise a published gem can be built from work nobody else has.
|
|
60
|
+
if `git branch -r --contains HEAD 2>/dev/null`.strip.empty?
|
|
61
|
+
problems << 'HEAD is not on any remote branch; push it before releasing'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
problems << "tag v#{VERSION} already exists; bump version.txt" if
|
|
65
|
+
`git tag -l v#{VERSION}`.strip == "v#{VERSION}"
|
|
66
|
+
|
|
67
|
+
problems << "History.txt has no entry for #{VERSION}" unless
|
|
68
|
+
File.read('History.txt').start_with?("== #{VERSION} ")
|
|
10
69
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
exclude ['.bnsignore', '.gitignore', '.ruby-gemset', '.ruby-version', '.travis.yml', 'vendor', '.git']
|
|
18
|
-
}
|
|
70
|
+
# A version number is the one genuinely unrecoverable resource in a
|
|
71
|
+
# release: RubyGems refuses to re-push it even after a yank.
|
|
72
|
+
if (published = rubygems_versions)
|
|
73
|
+
problems << "#{SPEC.name} #{VERSION} is already on rubygems.org" if
|
|
74
|
+
published.any? {|v| v['number'] == VERSION }
|
|
75
|
+
end
|
|
19
76
|
|
|
77
|
+
# Surfaced now rather than as a 403 after the tag has been pushed.
|
|
78
|
+
# Ask RubyGems where the credentials file lives rather than assuming
|
|
79
|
+
# ~/.gem/credentials: modern installs use the XDG path
|
|
80
|
+
# (~/.local/share/gem/credentials), and hardcoding the old one reports a
|
|
81
|
+
# missing key that is really there.
|
|
82
|
+
require 'rubygems/config_file'
|
|
83
|
+
creds = Gem.configuration.credentials_path
|
|
84
|
+
unless ENV['GEM_HOST_API_KEY'] || File.exist?(creds)
|
|
85
|
+
problems << "no rubygems credentials at #{creds}; run `gem signin` (the key needs the push_rubygem scope)"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
|
89
|
+
warn "WARNING: releasing from #{branch}, not master." unless branch == 'master'
|
|
90
|
+
|
|
91
|
+
unless problems.empty?
|
|
92
|
+
abort "ERROR: not ready to release #{SPEC.name} #{VERSION}:\n" +
|
|
93
|
+
problems.map {|p| " - #{p}" }.join("\n")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "OK: #{SPEC.name} #{VERSION} passes pre-release checks."
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc 'Build the gem and prove the built artifact actually works'
|
|
100
|
+
task :verify => :repackage do
|
|
101
|
+
require 'tmpdir'
|
|
102
|
+
|
|
103
|
+
puts "Packaged #{GEM_FILE}:"
|
|
104
|
+
Gem::Package.new(GEM_FILE).spec.files.each {|f| puts " #{f}" }
|
|
105
|
+
|
|
106
|
+
# Installing into a throwaway GEM_HOME is the only check that covers what
|
|
107
|
+
# was actually packaged rather than what is sitting in the working tree --
|
|
108
|
+
# a file missing from the manifest looks fine locally and breaks on install.
|
|
109
|
+
Dir.mktmpdir do |dir|
|
|
110
|
+
unbundled 'gem', 'install', '--no-document', '--install-dir', dir, GEM_FILE
|
|
111
|
+
|
|
112
|
+
script = <<~RUBY
|
|
113
|
+
gem 'btree'
|
|
114
|
+
require 'btree'
|
|
115
|
+
raise "version mismatch: \#{Btree.version} != #{VERSION}" unless Btree.version == '#{VERSION}'
|
|
116
|
+
t = Btree.create(3)
|
|
117
|
+
(1..50).each {|i| t[i] = i.to_s }
|
|
118
|
+
got = t[10..14]
|
|
119
|
+
raise "bad range query: \#{got.inspect}" unless got == %w[10 11 12 13 14]
|
|
120
|
+
raise "bad lookup" unless t[37] == '37'
|
|
121
|
+
puts "smoke test passed on the installed gem"
|
|
122
|
+
RUBY
|
|
123
|
+
|
|
124
|
+
Dir.chdir(dir) do
|
|
125
|
+
unbundled({'GEM_HOME' => dir, 'RUBYOPT' => nil}, RbConfig.ruby, '-e', script)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
desc 'Tag the release and push the tag to origin (reversible)'
|
|
131
|
+
task :tag do
|
|
132
|
+
sh 'git', 'tag', '-a', "v#{VERSION}", '-m', "#{SPEC.name} #{VERSION}"
|
|
133
|
+
sh 'git', 'push', 'origin', "v#{VERSION}"
|
|
134
|
+
puts "Tagged v#{VERSION}. To undo: git tag -d v#{VERSION} && git push origin :v#{VERSION}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# repackage, not build: the packaged gem is a file task, so an existing
|
|
138
|
+
# pkg/*.gem newer than its sources satisfies :build without rebuilding. Fine
|
|
139
|
+
# locally, wrong when the next step is irreversible.
|
|
140
|
+
desc 'Push the gem to rubygems.org (IRREVERSIBLE)'
|
|
141
|
+
task :push => :repackage do
|
|
142
|
+
puts
|
|
143
|
+
puts "About to push #{GEM_FILE} to rubygems.org."
|
|
144
|
+
puts "This cannot be undone: #{SPEC.name} #{VERSION} can never be re-pushed,"
|
|
145
|
+
puts 'even if it is yanked afterwards.'
|
|
146
|
+
|
|
147
|
+
unless ENV['FORCE'] == '1'
|
|
148
|
+
abort 'ERROR: refusing to push from a non-interactive shell without FORCE=1.' unless $stdin.tty?
|
|
149
|
+
print "Type the version (#{VERSION}) to confirm: "
|
|
150
|
+
abort 'Aborted.' unless $stdin.gets.to_s.strip == VERSION
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sh 'gem', 'push', GEM_FILE
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
desc 'Confirm the version is live on rubygems.org'
|
|
157
|
+
task :confirm do
|
|
158
|
+
published = rubygems_versions
|
|
159
|
+
if published&.any? {|v| v['number'] == VERSION }
|
|
160
|
+
puts "Confirmed: #{SPEC.name} #{VERSION} is live on rubygems.org."
|
|
161
|
+
else
|
|
162
|
+
warn "WARNING: #{SPEC.name} #{VERSION} is not visible on rubygems.org yet (indexing can lag)."
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
desc 'Full release: check, test, verify the built gem, tag, then push'
|
|
168
|
+
task :release do
|
|
169
|
+
# Ordered so that everything reversible happens first. The tag is cheap to
|
|
170
|
+
# delete; the push is permanent, so it goes last and only after the packaged
|
|
171
|
+
# gem has been installed and exercised.
|
|
172
|
+
%w[release:check test release:verify release:tag release:push release:confirm].each do |t|
|
|
173
|
+
puts "\n==> #{t}"
|
|
174
|
+
Rake::Task[t].invoke
|
|
175
|
+
end
|
|
176
|
+
puts "\nReleased #{SPEC.name} #{VERSION}."
|
|
177
|
+
end
|
data/btree.gemspec
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = 'btree'
|
|
5
|
+
# version.txt stays the single source of truth: Btree.version reads it back
|
|
6
|
+
# at runtime, so it has to ship in the gem regardless.
|
|
7
|
+
s.version = File.read(File.expand_path('version.txt', __dir__)).strip
|
|
8
|
+
s.authors = ['Douglas A. Seifert']
|
|
9
|
+
s.email = 'doug@dseifert.net'
|
|
10
|
+
s.homepage = 'https://github.com/seifertd/Ruby-BTree'
|
|
11
|
+
s.license = 'MIT'
|
|
12
|
+
|
|
13
|
+
s.summary = 'Pure ruby implementation of a btree'
|
|
14
|
+
s.description = <<~DESC
|
|
15
|
+
Pure ruby implementation of a btree as described in Introduction to
|
|
16
|
+
Algorithms by Cormen, Leiserson, Rivest and Stein, Chapter 18.
|
|
17
|
+
DESC
|
|
18
|
+
|
|
19
|
+
# Array#bsearch_index, used by Node#key_index, is the oldest thing here that
|
|
20
|
+
# is not universally available. CI only exercises 3.2 and up, because that is
|
|
21
|
+
# the floor imposed by the development gems, not by the library.
|
|
22
|
+
s.required_ruby_version = '>= 2.3.0'
|
|
23
|
+
|
|
24
|
+
# Rejecting dotfiles drops .github/, .gitignore and the .ruby-* files, which
|
|
25
|
+
# is the same set the old Bones exclude list named by hand. Building outside
|
|
26
|
+
# a git checkout is not supported, which matches how this gem is released.
|
|
27
|
+
s.files = `git ls-files -z`.split("\x0").reject {|f| f.start_with?('.') }
|
|
28
|
+
|
|
29
|
+
s.require_paths = ['lib']
|
|
30
|
+
s.extra_rdoc_files = ['History.txt', 'README.md']
|
|
31
|
+
s.rdoc_options = ['--main', 'README.md']
|
|
32
|
+
|
|
33
|
+
s.metadata = {
|
|
34
|
+
'homepage_uri' => s.homepage,
|
|
35
|
+
'source_code_uri' => s.homepage,
|
|
36
|
+
'changelog_uri' => "#{s.homepage}/blob/master/History.txt",
|
|
37
|
+
'bug_tracker_uri' => "#{s.homepage}/issues",
|
|
38
|
+
|
|
39
|
+
# Requires that whoever pushes this gem has MFA enabled on their
|
|
40
|
+
# rubygems.org account, so a leaked API key is not enough on its own.
|
|
41
|
+
# This needs MFA turned on for the account first, or the push is rejected.
|
|
42
|
+
'rubygems_mfa_required' => 'true'
|
|
43
|
+
}
|
|
44
|
+
end
|
data/lib/btree/node.rb
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
class Btree::Node
|
|
2
|
+
|
|
3
|
+
# Nodes holding at most this many keys are searched by a straight scan
|
|
4
|
+
# rather than by bsearch, whose per-probe block dispatch costs more than the
|
|
5
|
+
# comparisons it saves on a short array. The measured crossover is around
|
|
6
|
+
# 23 keys and the curves are within a couple of percent of each other from
|
|
7
|
+
# roughly 19 to 23, so the exact value here matters little. Note that a
|
|
8
|
+
# degree of 12 or less can never exceed the limit (a node holds at most
|
|
9
|
+
# 2*degree - 1 keys), and that a node under a larger degree still scans
|
|
10
|
+
# while it is sparsely filled.
|
|
11
|
+
LINEAR_SCAN_LIMIT = 20
|
|
12
|
+
private_constant :LINEAR_SCAN_LIMIT
|
|
13
|
+
|
|
2
14
|
def initialize(degree)
|
|
3
15
|
@degree = degree
|
|
4
16
|
@keys = []
|
|
@@ -46,17 +58,27 @@ class Btree::Node
|
|
|
46
58
|
@keys.size
|
|
47
59
|
end
|
|
48
60
|
|
|
61
|
+
# Values whose keys fall in the range, in key order. Subtrees that cannot
|
|
62
|
+
# contain an in-range key are never visited.
|
|
49
63
|
def values_of(range)
|
|
50
64
|
|
|
51
65
|
result = Array.new
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
lo = range.begin
|
|
67
|
+
hi = range.end
|
|
68
|
+
|
|
69
|
+
# Skip the keys, and the subtrees between them, that sort entirely below
|
|
70
|
+
# the range. When every key is below it, this lands on the rightmost
|
|
71
|
+
# child, which may still hold in-range keys.
|
|
72
|
+
i = lo ? key_index(lo) : 0
|
|
73
|
+
|
|
74
|
+
result += @children[i].values_of(range) if !leaf? && @children[i]
|
|
75
|
+
|
|
76
|
+
while i < size
|
|
77
|
+
key = @keys[i].first
|
|
78
|
+
# Everything from here on sorts above the range.
|
|
79
|
+
break if hi && (range.exclude_end? ? key >= hi : key > hi)
|
|
80
|
+
result << @keys[i].last if range.cover? key
|
|
81
|
+
result += @children[i+1].values_of(range) if !leaf? && @children[i+1]
|
|
60
82
|
i += 1
|
|
61
83
|
end
|
|
62
84
|
|
|
@@ -69,47 +91,36 @@ class Btree::Node
|
|
|
69
91
|
|
|
70
92
|
return values_of(key) if key.kind_of? Range
|
|
71
93
|
|
|
72
|
-
i =
|
|
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}"
|
|
94
|
+
i = key_index(key)
|
|
78
95
|
|
|
79
|
-
if i
|
|
80
|
-
|
|
81
|
-
return @keys[i-1].last
|
|
96
|
+
if i < size && key == @keys[i].first
|
|
97
|
+
return @keys[i].last
|
|
82
98
|
elsif leaf?
|
|
83
|
-
#puts "We are a leaf, no more children, so val is nil"
|
|
84
99
|
return nil
|
|
85
100
|
else
|
|
86
|
-
|
|
87
|
-
return @children[i-1].value_of(key)
|
|
101
|
+
return @children[i].value_of(key)
|
|
88
102
|
end
|
|
89
103
|
end
|
|
90
104
|
|
|
91
105
|
def insert(key, value)
|
|
92
|
-
|
|
93
|
-
#
|
|
106
|
+
# The slot the key would occupy is also the slot it would already occupy if
|
|
107
|
+
# it were present, so locating it and rejecting a duplicate are one step.
|
|
108
|
+
# Checked at every node on the way down, not just at the leaf: a key that a
|
|
109
|
+
# split has promoted into an internal node would otherwise be descended
|
|
110
|
+
# past and inserted a second time, hiding one copy in a subtree.
|
|
111
|
+
i = key_index(key)
|
|
112
|
+
raise "Duplicate key" if i < size && @keys[i].first == key
|
|
113
|
+
|
|
94
114
|
if leaf?
|
|
95
|
-
|
|
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]
|
|
115
|
+
@keys.insert(i, [key, value])
|
|
101
116
|
else
|
|
102
|
-
|
|
103
|
-
i
|
|
117
|
+
if @children[i] && @children[i].full?
|
|
118
|
+
split(i)
|
|
119
|
+
# The split just promoted a key into this node, after the check above.
|
|
120
|
+
raise "Duplicate key" if @keys[i].first == key
|
|
121
|
+
i += 1 if key > @keys[i].first
|
|
104
122
|
end
|
|
105
|
-
|
|
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)
|
|
123
|
+
@children[i].insert(key, value)
|
|
113
124
|
end
|
|
114
125
|
end
|
|
115
126
|
|
|
@@ -160,4 +171,21 @@ class Btree::Node
|
|
|
160
171
|
@children
|
|
161
172
|
end
|
|
162
173
|
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
# Index of the first key that does not sort below the given one, or size if
|
|
177
|
+
# every key sorts below it. For an internal node that is also the index of
|
|
178
|
+
# the child to descend into. Both branches test only < so that keys need
|
|
179
|
+
# only support <, > and == -- bsearch's natural k >= key would widen that.
|
|
180
|
+
def key_index(key)
|
|
181
|
+
n = size
|
|
182
|
+
if n <= LINEAR_SCAN_LIMIT
|
|
183
|
+
i = 0
|
|
184
|
+
i += 1 while i < n && @keys[i].first < key
|
|
185
|
+
i
|
|
186
|
+
else
|
|
187
|
+
@keys.bsearch_index {|(k, _)| !(k < key) } || n
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
163
191
|
end
|
data/test/test_btree.rb
CHANGED
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
require 'minitest/autorun'
|
|
2
2
|
require 'btree'
|
|
3
|
-
require 'shoulda'
|
|
3
|
+
require 'shoulda/context'
|
|
4
|
+
|
|
5
|
+
class TestBtree < Minitest::Test
|
|
6
|
+
# Degree 2 tree with a real internal node:
|
|
7
|
+
# root [10, 20]
|
|
8
|
+
# children [5,6,7] [12,17] [30]
|
|
9
|
+
def multi_level_tree
|
|
10
|
+
t = Btree.create(2)
|
|
11
|
+
[10, 20, 5, 6, 12, 30, 7, 17].each {|k| t.insert(k, k.to_s)}
|
|
12
|
+
t
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_range_query_spans_multiple_children
|
|
16
|
+
t = multi_level_tree
|
|
17
|
+
assert_equal %w[5 6 7 10 12 17], t.value_of(5..17)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_range_query_finds_keys_in_rightmost_child
|
|
21
|
+
t = Btree.create(2)
|
|
22
|
+
(1..10).each {|k| t.insert(k, k.to_s)}
|
|
23
|
+
assert_equal %w[8 9 10], t.value_of(8..10)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_insert_duplicate_of_internal_key
|
|
27
|
+
t = multi_level_tree
|
|
28
|
+
internal_key = t.root.keys.first
|
|
29
|
+
refute t.root.leaf?, "fixture should have an internal root"
|
|
30
|
+
assert_raises(RuntimeError) do
|
|
31
|
+
t.insert(internal_key, "duplicate")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_duplicate_of_internal_key_does_not_corrupt_size
|
|
36
|
+
t = multi_level_tree
|
|
37
|
+
internal_key = t.root.keys.first
|
|
38
|
+
size_before = t.size
|
|
39
|
+
begin
|
|
40
|
+
t.insert(internal_key, "duplicate")
|
|
41
|
+
rescue RuntimeError
|
|
42
|
+
# expected once duplicate detection descends past the leaves
|
|
43
|
+
end
|
|
44
|
+
assert_equal size_before, t.size
|
|
45
|
+
assert_equal internal_key.to_s, t.value_of(internal_key)
|
|
46
|
+
end
|
|
4
47
|
|
|
5
|
-
class TestBtree < MiniTest::Test
|
|
6
48
|
def test_insert_notfull
|
|
7
49
|
t = Btree.create(5)
|
|
8
50
|
t.insert(5, "5")
|
data/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.1
|
metadata
CHANGED
|
@@ -1,52 +1,44 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: btree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Douglas A. Seifert
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: 3.8.4
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: 3.8.4
|
|
27
|
-
description: "Pure ruby implementation of a btree as described in Introduction to
|
|
28
|
-
Algorithms by \nCormen, Leiserson, Rivest and Stein, Chapter 18."
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Pure ruby implementation of a btree as described in Introduction to
|
|
14
|
+
Algorithms by Cormen, Leiserson, Rivest and Stein, Chapter 18.
|
|
29
15
|
email: doug@dseifert.net
|
|
30
16
|
executables: []
|
|
31
17
|
extensions: []
|
|
32
18
|
extra_rdoc_files:
|
|
33
19
|
- History.txt
|
|
20
|
+
- README.md
|
|
34
21
|
files:
|
|
35
22
|
- Gemfile
|
|
36
23
|
- Gemfile.lock
|
|
37
24
|
- History.txt
|
|
38
25
|
- README.md
|
|
39
26
|
- Rakefile
|
|
27
|
+
- btree.gemspec
|
|
40
28
|
- lib/btree.rb
|
|
41
29
|
- lib/btree/node.rb
|
|
42
30
|
- lib/btree/tree.rb
|
|
43
|
-
- lib/insert_alot.rb
|
|
44
31
|
- test/test_btree.rb
|
|
45
32
|
- version.txt
|
|
46
33
|
homepage: https://github.com/seifertd/Ruby-BTree
|
|
47
|
-
licenses:
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/seifertd/Ruby-BTree
|
|
38
|
+
source_code_uri: https://github.com/seifertd/Ruby-BTree
|
|
39
|
+
changelog_uri: https://github.com/seifertd/Ruby-BTree/blob/master/History.txt
|
|
40
|
+
bug_tracker_uri: https://github.com/seifertd/Ruby-BTree/issues
|
|
41
|
+
rubygems_mfa_required: 'true'
|
|
50
42
|
rdoc_options:
|
|
51
43
|
- "--main"
|
|
52
44
|
- README.md
|
|
@@ -56,18 +48,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
56
48
|
requirements:
|
|
57
49
|
- - ">="
|
|
58
50
|
- !ruby/object:Gem::Version
|
|
59
|
-
version:
|
|
51
|
+
version: 2.3.0
|
|
60
52
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
53
|
requirements:
|
|
62
54
|
- - ">="
|
|
63
55
|
- !ruby/object:Gem::Version
|
|
64
56
|
version: '0'
|
|
65
57
|
requirements: []
|
|
66
|
-
|
|
67
|
-
rubygems_version: 2.6.12
|
|
68
|
-
signing_key:
|
|
58
|
+
rubygems_version: 4.0.17
|
|
69
59
|
specification_version: 4
|
|
70
|
-
summary: Pure ruby implementation of a btree
|
|
71
|
-
|
|
72
|
-
test_files:
|
|
73
|
-
- test/test_btree.rb
|
|
60
|
+
summary: Pure ruby implementation of a btree
|
|
61
|
+
test_files: []
|