# Author: Lou Scoras <louis.j.scoras / gmail.com>
# Date:   Sun Nov 26 10:43:34 EST 2006
#
# q103.rb - Solution to Rubyquiz 103 (DictionaryMatcher)
#
# Implements DictionaryMatcher using a Trie.  This version of
# DictionaryMatcher only matches complete words, but it wouldn't
# be too hard to modify to match any substring.

class Trie
  def initialize
    @children = Hash.new {|h,k| h[k] = Trie.new}
  end

  attr_accessor :value

  def []=(key, value)
    insert(key, 0, value)
    key
  end

  def [](key)
    get(key,0)
  end

  def each &blk
    _each &blk
  end

  include Enumerable

  def keys
    inject([]) {|keys,(k,v)| keys << k; keys}
  end

  def values
    inject([]) {|vals,(k,v)| vals << v; vals}
  end

  def each_key &blk
    keys.each &blk
  end

  def each_value &blk
    values.each &blk
  end

  def inspect(indent=0)
    buff = ''
    i = ' ' * indent
    buff << i + "value: #{value}\n" if value
    return buff unless @children.size > 0
    @children.each {|k,c|
      buff << "#{i}#{k} =>\n"
      buff << c.inspect(indent+2)
    }
    buff
  end

  protected

  def _each(key='', &blk)
    blk.call(key,value) if key != '' and value
    @children.keys.sort.each do |k|
      @children[k]._each(key + k,&blk)
    end
  end

  def insert(key,offset,value)
    if offset == key.length - 1
      @children[key[offset,1]].value = value
    else
      @children[key[offset,1]].insert(key,offset+1,value)
    end
  end

  def get(key,offset)
    if offset == key.length - 1
      @children[key[offset,1]].value
    else
      return nil unless @children.has_key?(key[offset,1])
      @children[key[offset,1]].get(key,offset+1)
    end
  end
end

class DictionaryMatcher
  def initialize(opts={})
    @ignore_case = opts[:ignore_case]
    @trie = Trie.new
  end

  def ignores_case?
    @ignore_case
  end

  def <<(word)
    word = word.downcase if ignores_case?
    @trie[word] = true
  end

  def words
    @trie.keys
  end

  def include?(word)
    !@trie[(ignores_case?? word.downcase : word)].nil?
  end

  def =~(string)
    words = string.split
    positions = words.inject({}) { |h,w|
      h[w] = string.index(w) unless h[w]; h
    }
    words.each do |word|
      return positions[word] if
	 include?(ignores_case?? word.downcase : word)
    end
    return nil
  end

  alias_method :===,   :=~
  alias_method :match, :=~
end