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

More than 3 years have passed since last update.


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