查找连续的子字符串索引

时间:2011-04-19 15:22:10

标签: ruby autocomplete

给定搜索字符串和结果字符串(保证包含搜索字符串的所有字母,按顺序不区分大小写),如何最有效地获取表示结果字符串中的索引的范围数组到搜索字符串中的字母?

期望的输出:

substrings( "word", "Microsoft Office Word 2007" )
#=> [ 17..20 ]

substrings( "word", "Network Setup Wizard" )
#=> [ 3..5, 19..19 ]
#=> [ 3..4, 18..19 ]   # Alternative, acceptable, less-desirable output

substrings( "word", "Watch Network Daemon" )
#=> [ 0..0, 10..11, 14..14 ]

这适用于自动填充搜索框。这是a tool类似于Quicksilver的屏幕截图,用于强调字母,因为我正在寻找。请注意 - 与我上面的理想输出不同 - 此屏幕截图不喜欢更长的单一匹配 Screenshot of Colibri underlining letters in search results

基准测试结果

对当前工作结果进行基准测试表明,@ tokland基于正则表达式的答案基本上与我提出的基于StringScanner的解决方案一样快,代码更少:

               user     system      total        real
phrogz1    0.889000   0.062000   0.951000 (  0.944000)
phrogz2    0.920000   0.047000   0.967000 (  0.977000)
tokland    1.030000   0.000000   1.030000 (  1.035000)

以下是基准测试:

a=["Microsoft Office Word 2007","Network Setup Wizard","Watch Network Daemon"]
b=["FooBar","Foo Bar","For the Love of Big Cars"]
test = { a=>%w[ w wo wor word ], b=>%w[ f fo foo foobar fb fbr ] }
require 'benchmark'
Benchmark.bmbm do |x|
  %w[ phrogz1 phrogz2 tokland ].each{ |method|
    x.report(method){ test.each{ |words,terms|
      words.each{ |master| terms.each{ |term|
        2000.times{ send(method,term,master) }
      } }
    } }
  }
end

5 个答案:

答案 0 :(得分:3)

要有一些事情要做,那怎么样?

>> s = "word"
>> re = /#{s.chars.map{|c| "(#{c})" }.join(".*?")}/i # /(w).*?(o).*?(r).*?(d)/i/
>> match = "Watch Network Daemon".match(re)
=> #<MatchData "Watch Network D" 1:"W" 2:"o" 3:"r" 4:"D">
>> 1.upto(s.length).map { |idx| match.begin(idx) }
=> [0, 10, 11, 14]

现在你只需要build the ranges(如果你真的需要它们,我猜各个索引也可以。)

答案 1 :(得分:2)

Ruby的Abbrev模块是一个很好的起点。它将字符串分解为一个哈希值,该哈希值包含可以识别完整单词的唯一键:

require 'abbrev'
require 'pp'

abbr = Abbrev::abbrev(['ruby'])
>> {"rub"=>"ruby", "ru"=>"ruby", "r"=>"ruby", "ruby"=>"ruby"}

对于每个按键,您都可以查找并查看是否匹配。我会过滤掉短于一定长度的所有密钥,以减少哈希的大小。

这些键还会为您提供一组快速单词,以便在原始字符串中查找子词匹配。

快速查找以查看是否存在子字符串匹配:

regexps = Regexp.union(
  abbr.keys.sort.reverse.map{ |k|
    Regexp.new(
      Regexp.escape(k),
      Regexp::IGNORECASE
    )
  }
)

请注意,它正在转义模式,允许输入字符,例如?*.,并将其视为文字,而不是特殊字符用于正则表达式,就像他们通常会得到治疗一样。

结果如下:

/(?i-mx:ruby)|(?i-mx:rub)|(?i-mx:ru)|(?i-mx:r)/

Regexp的match将返回有关所发现内容的信息。

因为union“ORs”模式,它只会找到第一个匹配,这将是字符串中最短的匹配。为了解决这种逆转问题。

这应该会让你有一个良好的开端,你想做什么。


编辑:这是一些直接回答问题的代码。我们一直忙于工作,所以需要几天的时间来取回这个:

require 'abbrev'
require 'pp'

abbr = Abbrev::abbrev(['ruby'])
regexps = Regexp.union( abbr.keys.sort.reverse.map{ |k| Regexp.new( Regexp.escape(k), Regexp::IGNORECASE ) } )

target_str ='Ruby rocks, rub-a-dub-dub, RU there?'
str_offset = 0
offsets = []
loop do
  match_results = regexps.match(target_str, str_offset)
  break if (match_results.nil?)
  s, e = match_results.offset(0)
  offsets << [s, e - s]
  str_offset = 1 + s
end

pp offsets

>> [[0, 4], [5, 1], [12, 3], [27, 2], [33, 1]]

如果您希望范围替换为offsets << [s, e - s]并将offsets << [s .. e]返回:

>> [[0..4], [5..6], [12..15], [27..29], [33..34]]

答案 2 :(得分:2)

这是一个迟到的进入者,因为它接近终点线。

<强>码

def substrings( search_str, result_str )
  search_chars = search_str.downcase.chars
  next_char = search_chars.shift
  result_str.downcase.each_char.with_index.take_while.with_object([]) do |(c,i),a|
    if next_char == c
      (a.empty? || i != a.last.last+1) ? a << (i..i) : a[-1]=(a.last.first..i)
      next_char = search_chars.shift
    end   
    next_char
  end
end

<强>演示

substrings( "word", "Microsoft Office Word 2007" ) #=> [17..20]
substrings( "word", "Network Setup Wizard" )       #=> [3..5, 19..19]
substrings( "word", "Watch Network Daemon" )       #=> [0..0, 10..11, 14..14]

<强>基准

              user     system      total        real
phrogz1   1.120000   0.000000   1.120000 (  1.123083)
cary      0.550000   0.000000   0.550000 (  0.550728)

答案 3 :(得分:0)

我认为没有任何内置方法可以真正帮助解决这个问题,最好的方法是查看您正在搜索的单词中的每个字母并手动构建范围。你的下一个最佳选择可能是建立一个像@ tokland的回答一样的正则表达式。

答案 4 :(得分:0)

这是我的实施:

require 'strscan'
def substrings( search, master )
  [].tap do |ranges|
    scan = StringScanner.new(master)
    init = nil
    last = nil
    prev = nil
    search.chars.map do |c|
      return nil unless scan.scan_until /#{c}/i
      last = scan.pos-1
      if !init || (last-prev) > 1
        ranges << (init..prev) if init
        init = last
      end
      prev = last
    end
    ranges << (init..last)
  end
end

这是使用其他实用方法的较短版本(@ tokland的答案也需要):

require 'strscan'
def substrings( search, master )
  s = StringScanner.new(master)
  search.chars.map do |c|
    return nil unless s.scan_until(/#{c}/i)
    s.pos - 1
  end.to_ranges
end

class Array
  def to_ranges
    return [] if empty?
    [].tap do |ranges|
      init,last = first
      each do |o|
        if last && o != last.succ
          ranges << (init..last)
          init = o
        end
        last = o
      end
      ranges << (init..last)
    end
  end
end