Edited at

Ruby ハッシュ配列の複数Keyでのソート

Rubyの小ネタです。

ハッシュ配列を複数Keyで順位をつけてソートしたいケースがあり、やり方を検討した時の備忘。

他におすすめの方法があれば、ご指摘もらえると嬉しいです。


前提


  • Ruby 2.6.3


対象データ

def rand_hash

{ k1: rand(10), k2: rand(10), k3: rand(10), k4: rand(10) }
end

def rand_hash_array(num)
num.times.map { rand_hash }
end

# このデータを k1 > k2 > k3 > k4 の順でソートしたい。
>> rand_hash_array(10)
=> [
{:k1=>6, :k2=>7, :k3=>9, :k4=>0},
{:k1=>8, :k2=>1, :k3=>0, :k4=>6},
{:k1=>5, :k2=>6, :k3=>7, :k4=>6},
{:k1=>6, :k2=>3, :k3=>1, :k4=>8},
{:k1=>6, :k2=>0, :k3=>2, :k4=>7},
{:k1=>8, :k2=>1, :k3=>5, :k4=>9},
{:k1=>3, :k2=>3, :k3=>7, :k4=>7},
{:k1=>0, :k2=>1, :k3=>5, :k4=>9},
{:k1=>5, :k2=>1, :k3=>1, :k4=>1},
{:k1=>6, :k2=>8, :k3=>8, :k4=>3}
]


方法1: Enumerable#sort

Enumerable#sort を使う

data.sort do |a, b|

comp1 = a[:k1] <=> b[:k1]
comp2 = a[:k2] <=> b[:k2]
comp3 = a[:k3] <=> b[:k3]
comp4 = a[:k4] <=> b[:k4]
next comp1 if comp1 != 0
next comp2 if comp2 != 0
next comp3 if comp3 != 0
next comp4 if comp4 != 0
-1
end

=> [{:k1=>0, :k2=>1, :k3=>5, :k4=>9}, {:k1=>3, :k2=>3, :k3=>7, :k4=>7}, {:k1=>5, :k2=>1, :k3=>1, :k4=>1}, {:k1=>5, :k2=>6, :k3=>7, :k4=>6}, {:k1=>6, :k2=>0, :k3=>2, :k4=>7}, {:k1=>6, :k2=>3, :k3=>1, :k4=>8}, {:k1=>6, :k2=>7, :k3=>9, :k4=>0}, {:k1=>6, :k2=>8, :k3=>8, :k4=>3}, {:k1=>8, :k2=>1, :k3=>0, :k4=>6}, {:k1=>8, :k2=>1, :k3=>5, :k4=>9}]


方法2: Enumerable#sort_by

Enumerable#sort_by を使う。

同一型として比較できるならソートキーを作ってブロックの中で指定する。

以下は文字列として各要素をソート順に連結したソートキーを作った解法例。

※各要素は2桁までの数値桁のデータが入っている前提。

data.sort_by { |h| '%02d%02d%02d%02d' % [h[:k1], h[:k2], h[:k3], h[:k4]] }

=> [{:k1=>0, :k2=>1, :k3=>5, :k4=>9}, {:k1=>3, :k2=>3, :k3=>7, :k4=>7}, {:k1=>5, :k2=>1, :k3=>1, :k4=>1}, {:k1=>5, :k2=>6, :k3=>7, :k4=>6}, {:k1=>6, :k2=>0, :k3=>2, :k4=>7}, {:k1=>6, :k2=>3, :k3=>1, :k4=>8}, {:k1=>6, :k2=>7, :k3=>9, :k4=>0}, {:k1=>6, :k2=>8, :k3=>8, :k4=>3}, {:k1=>8, :k2=>1, :k3=>0, :k4=>6}, {:k1=>8, :k2=>1, :k3=>5, :k4=>9}]


追記 (2019/07/03)

コメントをいただき、ソートキーは配列で指定するシンプルな書き方を教えてもらいました。

以下の方法で、配列で指定した要素を順に <=> 比較し、上の書き方より、桁考慮/意図しない文字列比較結果等の考慮が不要になるので優れている解法だと思っております。コメントありがとうございました。

data.sort_by { |h| h.values_at(:k1, :k2, :k3, :k4) }

=> [{:k1=>0, :k2=>1, :k3=>5, :k4=>9}, {:k1=>3, :k2=>3, :k3=>7, :k4=>7}, {:k1=>5, :k2=>1, :k3=>1, :k4=>1}, {:k1=>5, :k2=>6, :k3=>7, :k4=>6}, {:k1=>6, :k2=>0, :k3=>2, :k4=>7}, {:k1=>6, :k2=>3, :k3=>1, :k4=>8}, {:k1=>6, :k2=>7, :k3=>9, :k4=>0}, {:k1=>6, :k2=>8, :k3=>8, :k4=>3}, {:k1=>8, :k2=>1, :k3=>0, :k4=>6}, {:k1=>8, :k2=>1, :k3=>5, :k4=>9}]