Boyer-Moore string search algorithm in ruby

This was published on a personal blog I ran during the nillies, it’s included here as a historical archive. Links and images may be broken, it’s been a while.

Update: I cleaned the code up a bit and added more comments.

I was looking for a fast algorithms to search for matching strings. Boyer-Moore seems to be a good choice. It has the peculiar property that it gets faster once you’re looking for longer strings. Wikipedia: Boyer–Moore string search algorithm. This site also has a good explanation with graphical examples: Boyer-Moore algorithm

I translated the wikipedia example implementation to Ruby to gain more insight, and I’d like to share.

#############################################################
# Boyer-Moore for Ruby                                      #
# Author: Arne Brasseur (myfirstname@firstnamelastname.net) #
#                                                           #
# Licence: public domain                                    #
#                                                           #
#############################################################

# Some helpers

class String  

  # Take first n characters
  def first(n=1)  
    self.unpack('U*').first(n).pack('U*')
  end  

  # Take last n characters
  def last(n=1)  
    self.unpack('U*').last(n).pack('U*')
  end  
end

module Kernel

  # Shorthand
  def max(i1,i2); i1 > i2 ? i1 : i2; end
end


# This implementation is based on the C implementation found
# on Wikipedia. The purpose is mostly educational, since 
# String#index has the same functionality. If you want String#index
# to use this algorithm, include this module in String.
#
# It is assumed your strings are UTF-8, $KCODE is ignored.

module BoyerMoore    

  # return the position of needle in haystack, or nil if not found
  def self.search(haystack, needle)

    # Work on Arrays containing Unicode codepoint indices
    needle, haystack = needle.unpack('U*'), haystack.unpack('U*')
    
    # Maps a position in needle to a number of bytes to shift 
    # should the preceding byte differ  
    skip = []  
    
    # Maps characters in needle to their last index in needle
    occ = Hash.new {-1}
    
    return unless needle.length > 0;  
    
    #Preprocess #1: init occ[]  
    needle[0..-2].each_with_index{|c,i| occ[c]=i}  
    
    #Preprocess #2: init skip[]  
    needle.length.times do |i|  
      value=0  
      while (value < needle.length && !needlematch(needle, i, value)) do  
        value+=1  
      end  
      skip[needle.length-i-1] = value  
    end  
    
    #Search  
    hpos=0  
    while (hpos <= haystack.length - needle.length) do  
      npos = needle.length-1  
      
      # traverse the needle in reverse, if all bytes match we have a winner  
      while (needle[npos] == haystack[npos+hpos]) do
        return hpos if npos==0  
        npos -= 1;  
      end  

      # otherwise shift, either based on skip[] or based on occ[]  
      hpos += max(skip[npos], npos - occ[haystack[npos+hpos]]);
    end  
  end

  # Alternative index method for String
  def index(needle)
    BoyerMoore.search(self, needle)
  end
  
  private
    def self.needlematch(needle, length, offset)  
      #cut off offset bytes from needle  
      needle_begin = needle.first(needle.length-offset)  
      
      #if both needle and needle_begin contain at least length+1 bytes 
      if (needle_begin.length > length)  
        needle[-length-1] != needle_begin[-length-1]  &&  
          needle.last(length) == needle_begin.last(length)  
      else  
        needle.last(needle_begin.length) == needle_begin  
      end  
    end

end

#class String
#  include BoyerMoore
#end

# example

needle='abcab'
['12abcabc', 'abcgghhhaabcabccccc', '123456789abc123abc', 'aabbcc'].each do |hay|
    puts "#{BoyerMoore.search(hay, needle)} -- #{hay.index(needle)}"
end

It’s already more idiomatic Ruby than the original, but I believe it can be made even more so. I tried to make a readable implementation so it’s easy to grasp what the algorithm is doing. Hence the prefix/suffix methods of String.