diff --git a/lib/strokedb/data_structures/simple_skiplist.rb b/lib/strokedb/data_structures/simple_skiplist.rb index 335fab9a..dc04b09a 100644 --- a/lib/strokedb/data_structures/simple_skiplist.rb +++ b/lib/strokedb/data_structures/simple_skiplist.rb @@ -42,11 +42,9 @@ def empty? !node_next(@head, 0) end - # Complicated search algorithm. - # Implementation plan: - # 1. no reverse, no with_keys - just find a start_key and move until end_key or limit. - # 2. with_keys support - # 3. reverse support + # Complicated search algorithm + # TODO: add reverse support + # TODO: add key duplication support # def search(start_key, end_key, limit, offset, reverse, with_keys) offset ||= 0 @@ -64,10 +62,22 @@ def search(start_key, end_key, limit, offset, reverse, with_keys) # def find_by_prefix(start_key, reverse) # TODO: add reverse support - !start_key and return node_next(node_first, 0) - x = find_nearest_node(start_key) - start_key and node_key(x)[0, start_key.size] != start_key and return nil - x + x = node_first # head [FIXME: change method name] + # if no prefix given, just return a first node + !start_key and return node_next(x, 0) + + level = node_level(x) + while level > 0 + level -= 1 + xnext = node_next(x, level) + while node_compare(xnext, start_key) < 0 + x = xnext + xnext = node_next(x, level) + end + end + xnext == @tail and return nil + node_key(xnext)[0, start_key.size] != start_key and return nil + xnext end # @@ -76,10 +86,10 @@ def skip_nodes(node, offset, reverse) # TODO: add reverse support tail = @tail while offset > 0 && node != tail - node = node_next(x, 0) + node = node_next(node, 0) offset -= 1 end - offset == 0 ? node : nil + offset <= 0 ? node : nil end # @@ -90,10 +100,10 @@ def collect_values(x, end_prefix, limit, reverse, with_keys) meth = method(with_keys ? :node_pair : :node_value) tail = @tail limit ||= Float::MAX + end_prefix ||= "" + pfx_size = end_prefix.size while x != tail - if end_prefix - node_compare(x, end_prefix) > 0 and return values - end + node_key(x)[0, pfx_size] > end_prefix and return values values.size >= limit and return values values << meth.call(x).freeze x = node_next(x, 0) diff --git a/spec/lib/strokedb/data_structures/simple_skiplist_spec.rb b/spec/lib/strokedb/data_structures/simple_skiplist_spec.rb index d4d6b17c..f482bd25 100644 --- a/spec/lib/strokedb/data_structures/simple_skiplist_spec.rb +++ b/spec/lib/strokedb/data_structures/simple_skiplist_spec.rb @@ -193,10 +193,10 @@ end end - describe "SimpleSkiplist#search" do + describe "SimpleSkiplist#search [#{lang}]" do before(:each) do @list = SimpleSkiplist.new - @keys = %w[ a aa ab b ba bb x xx xy xyz ] + @keys = %w[ a aa ab b ba bb pfx1 pfx2 pfx3 x xx xy xyz ] @values = @keys.map{|v| v + " value"} @key_values = @keys.map{|v| [v, v + " value"]} @key_values.each do |k, v| @@ -205,13 +205,84 @@ end it "should find all items" do - @list.search(nil, nil, nil, nil, nil, nil).should == @values + search_should_yield(@key_values) end - it "should find all items with keys" do - @list.search(nil, nil, nil, nil, nil, true).should == @key_values + it "should find all items starting with a prefix" do + search_should_yield(@key_values, :start_key => "a") + search_should_yield(@key_values[1..-1], :start_key => "aa") + search_should_yield(@key_values[2..-1], :start_key => "ab") + search_should_yield(@key_values[3..-1], :start_key => "b") + search_should_yield(@key_values[-1..-1], :start_key => "xyz") + search_should_yield(@key_values[6..-1], :start_key => "pfx") end + it "should not find any items if prefix not matched" do + search_should_yield([], :start_key => "middle") + search_should_yield([], :start_key => "__prefix") + search_should_yield([], :start_key => "zuffix") + search_should_yield([], :start_key => "pfx0") + end + + it "should not find all items before the given end_key (inclusive)" do + search_should_yield(@key_values, :end_key => "xyz") + search_should_yield(@key_values, :end_key => "zuffix") + search_should_yield(@key_values[0..-3], :end_key => "xx") + search_should_yield(@key_values[0..2], :end_key => "a") + search_should_yield(@key_values[0..1], :end_key => "aa") + search_should_yield(@key_values[0..5], :end_key => "b") + end + + it "should find items in a range" do + search_should_yield(@key_values[1..5], :start_key => "aa", :end_key => "bb") + search_should_yield(@key_values[0..2], :start_key => "a", :end_key => "a") + search_should_yield(@key_values[3..5], :start_key => "b", :end_key => "b") + search_should_yield(@key_values[6..8], :start_key => "pfx", :end_key => "pfx") + end + + it "should not find items in an invalid range" do + search_should_yield([], :start_key => "b", :end_key => "a") + search_should_yield([], :start_key => "z", :end_key => "a") + search_should_yield([], :start_key => "_", :end_key => "b") + search_should_yield([], :start_key => "ab1", :end_key => "b") + end + + + def search_should_yield(results, os = {}) + # TODO: added reverse cases + list = @list + + os = os.merge(:with_keys => true) + r = search_with_options(list, os) + r.should == results + search_should_use_offsets_and_limits(list, os, r) + + os = os.merge(:with_keys => nil) + r = search_with_options(list, os) + r.should == results.map{|k,v| v} + search_should_use_offsets_and_limits(list, os, r) + end + + def search_should_use_offsets_and_limits(list, os, r1) + + offsets = [-1000, -1, 0, 1, 2, 3, 4, 1000] + limits = [-1000, -1, 0, 1, 2, 3, 4, 1000] + + offsets.each do |off| + limits.each do |lim| + r2 = search_with_options(list, os.merge(:offset => off, :limit => lim)) + r2.should == (r1[off < 0 ? 0 : off, lim < 0 ? 0 : lim] || []) + end + end + end + + def search_with_options(list, os = {}) + #puts "OPTIONS: #{os.inspect}" + r = list.search(os[:start_key], os[:end_key], os[:limit], os[:offset], os[:reverse], os[:with_keys]) + #puts "R: #{r.inspect}" + r + end + end end