Ruby | did_you_mean gem でスペルミスを検出+ did_you_mean gem のコードリーディング
概要
スペルミスを検出する gem、 did_you_mean
が話題になっていたので早速試してみます。
また、実装が気になったのでコードリーディングもしてみます。
名前の類似度はレーベンシュタイン距離で算出している。
レーベンシュタイン距離 Wikipedia
文字数比でレーベンシュタイン距離が30%以上離れていなければ類似候補として表示する。
候補が複数ある場合は、複数表示。
インストール
$ gem install did_you_mean --no-ri --no-doc
$ gem list | grep did_you_mean
did_you_mean (0.7.0)
試行
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 }
- 出力
$ 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
interception gem
レーベンシュタイン距離
文字数の類似度の判定はレーベンシュタイン距離を利用しているようです。
レーベンシュタイン距離 Wikipedia
did_you_mean gem / levenshtein.rb
マジでプルリクする5秒前
MP5(※ MK5)。踏みとどまりました。
その1
did_you_mean gem / word_collection.rb、14-18行目
イテレーターから外だしできそうな処理があったのでリファクタリングしてみた。
計測してみたけど、たいした効果がなかったのでプルリクエストはやめました。
- before
def similar_to(target_word)
select do |word|
Levenshtein.distance(word.to_s, target_word.to_s) <= threshold(target_word)
end
end
- after
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行目
DidYouMean::Levenshtein.distance メソッド内のループ処理で、 each_with_index 関連のところを
簡潔にできるかな・・・と思ったのですが、あえて一時変数を利用している可能性もありそうなのでプルリクエストはやめました。
- before
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
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
interception GitHub
スペルミスでエラーが出たら、正しい名前を教えてくれる gem を作った
レーベンシュタイン距離 Wikipedia