Ruby | did_you_mean gem でスペルミスを検出＋ did_you_mean gem のコードリーディング

# Ruby | did_you_mean gem でスペルミスを検出＋ did_you_mean gem のコードリーディング

## 概要
スペルミスを検出する gem、 `did_you_mean` が話題になっていたので早速試してみます。
また、実装が気になったのでコードリーディングもしてみます。

名前の類似度はレーベンシュタイン距離で算出している。
[レーベンシュタイン距離 Wikipedia](http://ja.wikipedia.org/wiki/レーベンシュタイン距離)

文字数比でレーベンシュタイン距離が30%以上離れていなければ類似候補として表示する。
候補が複数ある場合は、複数表示。

## インストール
~~~bash
$ gem install did_you_mean --no-ri --no-doc
$ gem list | grep did_you_mean
did_you_mean (0.7.0)
~~~

## 試行

~~~ruby
require 'did_you_mean'

module Hogeable
  def hogeable
    "hogeable"
  end

  def hogec
    "hogec"
  end
end

class Hoge
  include Hogeable
  include Enumerable
  def hoge
    "hoge"
  end

  def hige
    "hige"
  end

  def hage
    "hage"
  end

  def bar1234567
  end
end
def call_did_you_mean_for_method(instance, m)
  instance.send(:"#{m}")
rescue => e
  puts e
end

def call_did_you_mean
  yield
rescue => e
  puts e
end

hoge = Hoge.new
call_did_you_mean_for_method(hoge, :hegeabl)
call_did_you_mean_for_method(hoge, :hogeabl)
call_did_you_mean_for_method(hoge, :hagec)
call_did_you_mean_for_method(hoge, :hege)
call_did_you_mean_for_method(hoge, :hogeabl)
call_did_you_mean { Hage.new }
call_did_you_mean { Arrai.new }
call_did_you_mean { Hogeabl.class }
~~~

* 出力

~~~bash
$ ruby did_you_mean.rb
undefined method `hegeabl' for #<Hoge:0x000006002d8650>

    Did you mean? #hogeable

undefined method `hogeabl' for #<Hoge:0x000006002d8650>

    Did you mean? #hoge
                  #hogeable
                  #hogec

undefined method `hagec' for #<Hoge:0x000006002d8650>

    Did you mean? #hoge
                  #hige
                  #hage
                  #hogec

undefined method `hege' for #<Hoge:0x000006002d8650>

    Did you mean? #hoge
                  #hige
                  #hage
                  #hogec

undefined method `hogeabl' for #<Hoge:0x000006002d8650>

    Did you mean? #hoge
                  #hogeable
                  #hogec

uninitialized constant Hage

    Did you mean? Hash
                  Range
                  Hoge

uninitialized constant Arrai

    Did you mean? Array

uninitialized constant Hogeabl

    Did you mean? Hogeable
                  Hoge
~~~

## コードリーディング
### NameErrorの捕捉
interception gem を利用して例外をインターセプトしている。
[did_you_mean gem / did_you_mean.rb](https://github.com/yuki24/did_you_mean/blob/master/lib/did_you_mean.rb)
[interception gem](https://github.com/ConradIrwin/interception)

### レーベンシュタイン距離
文字数の類似度の判定はレーベンシュタイン距離を利用しているようです。
[レーベンシュタイン距離 Wikipedia](http://ja.wikipedia.org/wiki/レーベンシュタイン距離)
[did_you_mean gem / levenshtein.rb](https://github.com/yuki24/did_you_mean/blob/master/lib/did_you_mean/levenshtein.rb)

## マジでプルリクする5秒前
MP5（※ [MK5](http://ja.wikipedia.org/wiki/MK5)）。踏みとどまりました。

### その1
[did_you_mean gem / word_collection.rb、14-18行目](https://github.com/yuki24/did_you_mean/blob/master/lib/did_you_mean/word_collection.rb#L14-L18)

イテレーターから外だしできそうな処理があったのでリファクタリングしてみた。
計測してみたけど、たいした効果がなかったのでプルリクエストはやめました。

* before

~~~ruby
def similar_to(target_word)
  select do |word|
    Levenshtein.distance(word.to_s, target_word.to_s) <= threshold(target_word)
  end
end
~~~

* after

~~~ruby
def similar_to(target_word)
  # to_s の呼び出しを メソッド数＝＞1回 に減らす
  target_word_str = target_word.to_s
  # threshold の呼び出しを メソッド数＝＞1回 に減らす
  thre = threshold(target_word)
  select do |word|
    Levenshtein.distance(word.to_s, target_word_str) <= thre
  end
end
~~~

### その2
[did_you_mean gem / levenshtein.rb 14-27行目](https://github.com/yuki24/did_you_mean/blob/master/lib/did_you_mean/levenshtein.rb#L14-L27)

DidYouMean::Levenshtein.distance メソッド内のループ処理で、 each_with_index 関連のところを
簡潔にできるかな・・・と思ったのですが、あえて一時変数を利用している可能性もありそうなのでプルリクエストはやめました。

* before

~~~ruby
str1.each_char.each_with_index do |char1, i|
  e = i + 1
  str2.each_char.each_with_index do |char2, j|
    cost = (char1 == char2) ? 0 : 1
    x = min3(
      d[j+1] + 1, # insertion
      e + 1,      # deletion
      d[j] + cost # substitution
    )
    d[j] = e
    e = x
  end
  d[m] = x
end
~~~

* after

~~~ruby
str1.each_char.with_index(1) do |char1, i|
  str2.each_char.each_with_index do |char2, j|
    cost = (char1 == char2) ? 0 : 1
    x = min3(
      d[j+1] + 1, # insertion
      i + 1,      # deletion
      d[j] + cost # substitution
    )
    d[j] = i
    i = x
  end
  d[m] = x
end
~~~

## 参照
[did_you_mean GitHub](https://github.com/yuki24/did_you_mean)
[interception GitHub](https://github.com/ConradIrwin/interception)
[スペルミスでエラーが出たら、正しい名前を教えてくれる gem を作った](http://qiita.com/yuki24/items/feb72ee1f280434d5a5f)
[レーベンシュタイン距離 Wikipedia](http://ja.wikipedia.org/wiki/レーベンシュタイン距離)