Rubyのmethod_missingを使って、似ているメソッド名を提案するコードを書いてみた

Rubyのmethod_missingを使って、呼び出したメソッドが存在しないときに一番似ているメソッド名を提示するコードを書きました。



少しだけ用語解説


method_missing(method, *args)

呼び出したメソッドが存在しない場合に、代わりに呼ばれるメソッドです。

呼び出したメソッド名、そのときのメソッド引数を全て受け取ることができます。


ActiveRecordのfind_by_[column]はこれを使って実装されてるみたいです。


詳しい説明は下記のページが分かりやすかったです。

Rubyのmethod_missingを使って黒魔術を実装する



似ているメソッド名を提案するコード


短いので全部書きます。


# -*- coding: utf-8 -*-

class MethodSuggestion < Array

  # 存在しないメソッドを呼び出した場合、一番名前の近いメソッドを見つける
  # 雑なので真似しては駄目な書き方です…。
  def method_missing(method, *args)
    distance_hash = Hash.new
    self.methods.each { |m|
      distance_hash[m] = ld(method, m)
    }
    near_methods = distance_hash.sort_by { |key, value| value }
    print(method.to_s, near_methods[0][0])
  end

  # 似ているメソッド名を提示する
  def print(call_method, similar_method)
    puts(%Q[ruby: '#{call_method}' is not a valid method.])
    puts ''
    puts 'Did you mean this?'
    puts "        #{similar_method}"
  end

  # 編集距離(レーベンシュタイン距離)を求める
  # 下記のURLのコードそのまま
  #
  # ruby でレーベンシュタイン距離(編集距離)の計算
  # http://d.hatena.ne.jp/kenkitii/20090204/ruby_levenshtein_distance
  def levenshtein_distance(str1, str2)
    col, row = str1.size + 1, str2.size + 1
    d = row.times.inject([]) { |a, i| a << [0] * col }
    col.times { |i| d[0][i] = i }
    row.times { |i| d[i][0] = i }

    str1.size.times do |i1|
      str2.size.times do |i2|
        cost = str1[i1] == str2[i2] ? 0 : 1
        x, y = i1 + 1, i2 + 1
        d[y][x] = [d[y][x-1]+1, d[y-1][x]+1, d[y-1][x-1]+cost].min
      end
    end
    d[str2.size][str1.size]
  end

  def ld(str1, str2)
    levenshtein_distance(str1, str2)
  end
end


# ここから実行開始
app = MethodSuggestion.new
puts app.includd?('aaa')


実行結果は下記の通り。


ruby: 'includd?' is not a valid method.

Did you mean this?
        include?


最近テストも練習中なので、簡単なテストも書いてみました。


# -*- coding: utf-8 -*-

require "rspec"
require_relative "../lib/method_suggestion.rb"

describe MethodSuggestion do

  before(:each) do
    @ms = MethodSuggestion.new
  end

  after(:each) do
    @ms = nil
  end

  describe "編集距離を計算するメソッド" do

    it "同じ文字列の場合は0が返ること" do
      @ms.ld('aaa', 'aaa') == 0
    end

    it "一文字違う文字列の場合は1が返ること" do
      @ms.ld('aaa', 'ada') == 1
    end

    it "Wikipediaと同じサンプル文字列の場合は3が返ること" do
      @ms.ld('kitten', 'sitting') == 3
    end

  end
end


まとめ


Rubyってほんとお手軽ですね。ちょっとしたコードを書くだけならとても便利です。


著者プロフィール
Webサイトをいくつか作っています。
著者プロフィール