複数の要素(オブジェクト)の同値性を特定の条件の下で比較したい、ということがたまにあります。

例えば、名前をキーとした住所録のデータベースを考えます。

ADDRESS = { Charlie:'228 Park Ave. NY', Liz:'1419 Westwood Blvd. CA', Scott:'50 Hasler Rd. WA' }

def ADDRESS.find_by_name(name)
  self[name]
end

ADDRESS.find_by_name(:Liz) # => "1419 Westwood Blvd. CA"

ここで、find_by_nameに渡す名前にそのuniqnessが保証される範囲で柔軟性を持たせる、つまり:lizでも’Liz’でも’LIZ’でも受け付けるよう対応するとします。こんな感じでしょうか。

def ADDRESS.find_by_name(name)
  self[name.capitalize.intern]
end

[:Liz, :liz, 'Liz', 'LIZ'].each do |name|
  puts ADDRESS.find_by_name(name)
end
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA

しかしながらデータベース側のキー値のフォーマットの一貫性が保証されていない場合、これは当然にうまく機能しません1

ADDRESS = { 'charlie' => '228 Park Ave. NY', Liz:'1419 Westwood Blvd. CA', Scott:'50 Hasler Rd. WA' }

def ADDRESS.find_by_name(name)
  self[name.capitalize.intern]
end

['charlie', 'Charlie', :Charlie, :charlie].each do |name|
  puts ADDRESS.find_by_name(name)
end
# >> 
# >> 
# >> 
# >>

このような場合に、比較する要素を同じ条件にしてその同値性を比較したという欲求が生まれます。

次のように対応するのでしょうか。

ADDRESS = { 'charlie' => '228 Park Ave. NY', Liz:'1419 Westwood Blvd. CA', Scott:'50 Hasler Rd. WA' }

def ADDRESS.find_by_name(name)
  res = self.detect do |kname, _|
    kname, name = [kname, name].map { |n| n.downcase.intern }
    kname == name
  end
  res ? res.last : nil 
end

[:Liz, :liz, 'Liz', 'LIZ'].each do |name|
  puts ADDRESS.find_by_name(name)
end

# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA

['charlie', 'Charlie', :Charlie, :charlie].each do |name|
  puts ADDRESS.find_by_name(name)
end

# >> 228 Park Ave. NY
# >> 228 Park Ave. NY
# >> 228 Park Ave. NY
# >> 228 Park Ave. NY

うまくいきましたが、2つの要素を変換した結果を一旦変数に入れて、それらの同一性を比較するという処理が何かまどろっこしいですよね。

Array#same?

そんなわけで…

Array#same?というのを考えました:-)

class Array
  def same?(&blk)
    self.uniq(&blk).size==1
  end
end

same?を使うと先のコードは次のように簡潔になります。

def ADDRESS.find_by_name(name)
  res = self.detect do |kname, _|
    [kname, name].same? { |n| n.downcase.intern }
  end
  res ? res.last : nil 
end

[:Liz, :liz, 'Liz', 'LIZ'].each do |name|
  puts ADDRESS.find_by_name(name)
end

# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA

res ? res.last : nilもまどろっこしいという人は、「前回」紹介したtap+breakでさらに..

def ADDRESS.find_by_name(name)
  self.detect do |kname, _|
    [kname, name].same? { |n| n.downcase.intern }
  end.tap { |s| break s.last if s }
end

[:Liz, :liz, 'Liz', 'LIZ'].each do |name|
  puts ADDRESS.find_by_name(name)
end
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA
# >> 1419 Westwood Blvd. CA

Array#same?なんかあってもよさそうですけど、需要ないんですかね?

以上、Arrayの小ネタでした…

(追記:2012-10-31) タイトルを変更しました。


おんなじ、おんなじ!でも、ちょっとちがう! by ジェニー・スー コステキ=ショー

  1. まあそんな設計に問題があるんですが..


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS