ITエンジニアによるITエンジニアのためのブログ

Rubyで複数の条件でソートするsort_byメソッドの活用

複数条件によるソートとは、ある値によってソートした際にその値が同位な場合に、第二ソート、第三ソート、と別の値によって順位を決める方法です。

rubyでソートをする場合、sortとsort_byというEnumerableのメソッドがありますが、複数の条件でソートが可能なのは後者の方です。

単一の値によるソート

通常の1つのみの値によるソートの場合、以下のようにブロックで比較するスカラー値を返します。アルゴリズムは昇順で並び替えを行いますので、降順にしたい場合はマイナスサインを使う(数値の場合)などの方法で値を反転します。

books = [{ title: 'Ruby', author: 'Matz' }, { title: 'Rails', author: 'DHH' }]

# 昇順
books.sort_by { |book| book[:title].length }
=> [{:title=>"Ruby", :author=>"Matz"}, {:title=>"Rails", :author=>"DHH"}] 

# 降順
books.sort_by { |book| -book[:title].length }
=> [{:title=>"Rails", :author=>"DHH"}, {:title=>"Ruby", :author=>"Matz"}]

複数の値によるソート

第一ソート、第二ソートなど複数の値によってソートしたい場合は、sort_byでスカラー値ではなく配列を返します。

なぜ配列を返すのでしょうか? それは、Rubyのsort_byメソッドが、ブロックの戻り値を比較してソートを行うためです。Rubyでの配列の比較は、先頭の要素から順に比較が行われ、最初の異なる要素で大小関係が決まります。そのため、配列を返すことで、複数の要素を一度に比較し、辞書順のようなソートを実現できます。

例えば、以下の配列を、まず作者名の長さ、次に作品名の長さでソートする場合、次のように書けます。

books = [{ title: 'Orange', author: 'Zeus' }, { title: 'Apple', author: 'Zeus' }, { title: 'Zen', author: 'Ann' }]

books.sort_by { |book| [book[:author].length, book[:title].length] }
 => [{:title=>"Zen", :author=>"Ann"}, {:title=>"Apple", :author=>"Zeus"}, {:title=>"Orange", :author=>"Zeus"}]

また、それぞれのソート条件ごとに昇順、降順を組み合わせることもできます。

# 第一ソートは昇順、第二ソートは降順
books.sort_by { |book| [book[:author].length, -book[:title].length] }
=> [{:title=>"Zen", :author=>"Ann"}, {:title=>"Orange", :author=>"Zeus"}, {:title=>"Apple", :author=>"Zeus"}] 

なお、第二ソートだけでなく第三ソート、第四ソートなど複数のソート方法を指定できます。

おまけ:第二ソートを使った安定ソート

第一ソートで同位だった場合、通常のソートであれば元の順番が保持されるかは不確実です。但し第二ソートを応用することで、元の順番を確実に保持できるようになります。

books = [{ title: 'Orange', author: 'Zeus' }, { title: 'Apple', author: 'Zeus' }, { title: 'Zen', author: 'Ann' }]

n = 0
books.sort_by { |book| [book[:author].length, n += 1] }
[{:title=>"Zen", :author=>"Ann"}, {:title=>"Orange", :author=>"Zeus"}, {:title=>"Apple", :author=>"Zeus"}]

なぜn += 1をするのでしょうか? これは、元の並び順を数値で表現し、第二ソートの基準に加えることで、第一ソートで同位だった要素の元の順番を保持するためです。

まとめ

rubyのsort_byメソッドによる複数条件によるソート方法を解説しました。スカラー値ではなく配列を返すところがポイントなので、ぜひ覚えてみてください。