range_hash 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|