range_hash 2.0.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/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.md +62 -0
- data/Rakefile +10 -0
- data/lib/range_hash/version.rb +3 -0
- data/lib/range_hash.rb +101 -0
- data/range_hash.gemspec +25 -0
- data/spec/rangehash_spec.rb +101 -0
- data/spec/spec_helper.rb +3 -0
- metadata +81 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
range_hash
|
3
|
+
========================
|
4
|
+
|
5
|
+
##Problem
|
6
|
+
|
7
|
+
Given a list of ranges, how do you effectively find if an element exist in any of them?
|
8
|
+
|
9
|
+
##Solution
|
10
|
+
|
11
|
+
[sudo] gem install range_hash
|
12
|
+
|
13
|
+
##Example
|
14
|
+
|
15
|
+
### Simple
|
16
|
+
|
17
|
+
require 'range_hash'
|
18
|
+
ranges = RangeHash.new
|
19
|
+
ranges[1..3] = 'a'
|
20
|
+
ranges[10..15] = 'b'
|
21
|
+
ranges[27...30] = 'c'
|
22
|
+
ranges[7..10] = 'd'
|
23
|
+
|
24
|
+
ranges[2] #=> 'a'
|
25
|
+
ranges[15] #=> 'b'
|
26
|
+
ranges[100] #=> nil
|
27
|
+
|
28
|
+
### Callbacks
|
29
|
+
require 'range_hash'
|
30
|
+
ranges = RangeHash.new
|
31
|
+
ranges.add_callback(1..13) do |elem|
|
32
|
+
puts "#{elem} is between 1 and 3"
|
33
|
+
end
|
34
|
+
ranges.add_callback(10..15) do |elem|
|
35
|
+
puts "#{elem} is between 10 and 15!"
|
36
|
+
end
|
37
|
+
ranges.add_callback(27...30) do |elem|
|
38
|
+
puts "#{elem} is between 27 and 29!"
|
39
|
+
end
|
40
|
+
ranges.add_callback(7..10) do |elem|
|
41
|
+
puts "#{elem} is between 7 and 10!"
|
42
|
+
end
|
43
|
+
ranges.add_callback(:default) do |elem|
|
44
|
+
puts "Couldn't find a range for #{elem}"
|
45
|
+
end
|
46
|
+
|
47
|
+
ranges.call(2) #=> 2 is between 1 and 3!
|
48
|
+
ranges.call(15) #=> 15 is between 10 and 15!
|
49
|
+
ranges.call(100) #=> Couldn't find a range of 100
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
##Wait ... why?
|
54
|
+
|
55
|
+
Because O(log n) is better than O(N). RangeHash keeps an internal sorted array that uses a binary search to find your range. This allows you to achieve (log n) search. Another option would be to store a dense hash that stores all possible elements within the ranges, however this isn't efficient in terms of memory usage. This is great happy medium.
|
56
|
+
|
57
|
+
On-top of that, RangeHash provides some awesome functionality such as callbacks.
|
58
|
+
|
59
|
+
|
60
|
+
##Notes
|
61
|
+
|
62
|
+
Currently does not support ranges that overlap one another.
|
data/Rakefile
ADDED
data/lib/range_hash.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require "rangehash/version"
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
class Range
|
5
|
+
def real_end
|
6
|
+
exclude_end? ? self.end - 1 : self.end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class RangeHashElement
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :range
|
14
|
+
attr_accessor :value, :callback
|
15
|
+
|
16
|
+
def_delegators :@range, :begin
|
17
|
+
def_delegator :@range, :real_end, :end
|
18
|
+
def initialize(opts={})
|
19
|
+
@range, @value, @callback = opts[:range], opts[:value], opts[:callback]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class RangeHash
|
24
|
+
def initialize
|
25
|
+
@arr = []
|
26
|
+
@default_callback = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
result = search(key)
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(key)
|
34
|
+
index = search(key, true)
|
35
|
+
if index < 0
|
36
|
+
@default_callback.call(key) if @default_callback.respond_to?(:call)
|
37
|
+
else
|
38
|
+
callback = @arr[index].callback
|
39
|
+
callback.call(key) if callback.respond_to?(:call)
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(range,value)
|
45
|
+
raise ArgumentError.new("Key should be a range") unless Range === range
|
46
|
+
edit_or_create(range, :range => range, :value => value)
|
47
|
+
value
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_callback(range, &callback)
|
51
|
+
raise ArgumentError.new("Need to pass a callback in the form of a block") unless block_given?
|
52
|
+
unless Range === range || range == :default
|
53
|
+
raise ArgumentError.new("Argument needs to be a range or :default")
|
54
|
+
end
|
55
|
+
if range == :default
|
56
|
+
@default_callback = callback
|
57
|
+
else
|
58
|
+
edit_or_create(range, :range => range, :callback => callback)
|
59
|
+
end
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
"#RangeHash<@ranges=[#{@arr.map(&:range).join(",")}]>"
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def edit_or_create(range, element_opts={})
|
70
|
+
index = search(range.real_end, true)
|
71
|
+
if index < 0
|
72
|
+
elem = RangeHashElement.new(element_opts)
|
73
|
+
index = -(index + 1)
|
74
|
+
@arr.insert(index, elem)
|
75
|
+
else
|
76
|
+
element_opts.delete(:range)
|
77
|
+
edit_attr, attr_value = element_opts.to_a.first
|
78
|
+
@arr[index].instance_variable_set(:"@#{edit_attr}", attr_value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def search(key, ret_index=false)
|
83
|
+
low, high = 0, @arr.size - 1
|
84
|
+
mid = 0
|
85
|
+
|
86
|
+
while low <= high
|
87
|
+
mid = (low + high)/ 2
|
88
|
+
if key <= @arr[mid].end && key >= @arr[mid].begin
|
89
|
+
val = ret_index ? mid : @arr[mid].value
|
90
|
+
return val
|
91
|
+
elsif key < @arr[mid].end
|
92
|
+
high = mid - 1
|
93
|
+
else
|
94
|
+
low = mid + 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
return -low - 1 if ret_index
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
data/range_hash.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "range_hash/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "range_hash"
|
7
|
+
s.version = RangeHash::VERSION
|
8
|
+
s.authors = ["Mike Lewis"]
|
9
|
+
s.email = ["ft.mikelewis@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{An efficient hash that allows ranges to be keys and searched given an element within those ranges.}
|
12
|
+
s.description = %q{An efficient hash that allows ranges to be keys and searched given an element within those ranges.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "range_hash"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
# s.add_runtime_dependency "rest-client"
|
25
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RangeHash do
|
4
|
+
before do
|
5
|
+
@rh = RangeHash.new
|
6
|
+
end
|
7
|
+
[:[], :[]=].each do |meth|
|
8
|
+
it "should respond to #{meth}" do
|
9
|
+
@rh.should respond_to(meth)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context "integeration" do
|
14
|
+
before do
|
15
|
+
c = (13..16)
|
16
|
+
d = (20...25)
|
17
|
+
a = (1..10)
|
18
|
+
b = (10...13)
|
19
|
+
|
20
|
+
@rh[c] = 'c'
|
21
|
+
@rh[d] = 'd'
|
22
|
+
@rh[a] = 'a'
|
23
|
+
@rh[b] = 'b'
|
24
|
+
end
|
25
|
+
it "should insert to keep the internal array" do
|
26
|
+
internal_array = @rh.instance_eval { @arr }
|
27
|
+
internal_array.map(&:value).should == ['a', 'b', 'c', 'd']
|
28
|
+
end
|
29
|
+
|
30
|
+
it "[] should return the associated value for that range" do
|
31
|
+
@rh[21].should == 'd'
|
32
|
+
end
|
33
|
+
|
34
|
+
it "[] should return nil for a value that is not in a range" do
|
35
|
+
@rh[25].should == nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should beable to replace an element" do
|
39
|
+
@rh[(13..16)] = 'new'
|
40
|
+
|
41
|
+
@rh[14].should == 'new'
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should accept callbacks and return nil" do
|
45
|
+
result = nil
|
46
|
+
n = @rh.add_callback(100..105) do |elem|
|
47
|
+
result = elem
|
48
|
+
end
|
49
|
+
|
50
|
+
@rh.call(103)
|
51
|
+
|
52
|
+
result.should == 103
|
53
|
+
n.should be_nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should accept a default callback" do
|
57
|
+
result = nil
|
58
|
+
@rh.add_callback(:default) do |elem|
|
59
|
+
result = elem
|
60
|
+
end
|
61
|
+
@rh.call(2000)
|
62
|
+
result.should == 2000
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should return nil and not blow up when accessing a callback that does not exist" do
|
66
|
+
lambda { @rh.call(3) }.should_not raise_error
|
67
|
+
@rh.call(3).should be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should raise an ArgumentError if no block is passed into add_callback" do
|
71
|
+
lambda { @rh.add_callback((1..3)) }.should raise_error(ArgumentError)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should raise an ArgumentError if a non range or not default is passed into a callback" do
|
75
|
+
lambda { @rh.add_callback(3) }.should raise_error(ArgumentError)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should raise an ArgumentError if key is not range" do
|
79
|
+
lambda { @rh["yo"] = 5}.should raise_error(ArgumentError)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
describe RangeHashElement do
|
87
|
+
it "should handle inclusive ranges for end" do
|
88
|
+
e = RangeHashElement.new(:range => (1..10), :value => 'a')
|
89
|
+
e.end.should == 10
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should handle begin for ranges" do
|
93
|
+
e = RangeHashElement.new(:range => (1..10), :value => 'a')
|
94
|
+
e.begin.should == 1
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should handle exclusive ranges end" do
|
98
|
+
e = RangeHashElement.new(:range => (1...10), :value => 'a')
|
99
|
+
e.end.should == 9
|
100
|
+
end
|
101
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: range_hash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Lewis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &2151811980 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2151811980
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &2151810580 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2151810580
|
36
|
+
description: An efficient hash that allows ranges to be keys and searched given an
|
37
|
+
element within those ranges.
|
38
|
+
email:
|
39
|
+
- ft.mikelewis@gmail.com
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- .rspec
|
46
|
+
- Gemfile
|
47
|
+
- README.md
|
48
|
+
- Rakefile
|
49
|
+
- lib/range_hash.rb
|
50
|
+
- lib/range_hash/version.rb
|
51
|
+
- range_hash.gemspec
|
52
|
+
- spec/rangehash_spec.rb
|
53
|
+
- spec/spec_helper.rb
|
54
|
+
homepage: ''
|
55
|
+
licenses: []
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project: range_hash
|
74
|
+
rubygems_version: 1.8.10
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: An efficient hash that allows ranges to be keys and searched given an element
|
78
|
+
within those ranges.
|
79
|
+
test_files:
|
80
|
+
- spec/rangehash_spec.rb
|
81
|
+
- spec/spec_helper.rb
|