【Ruby】sort_by/sortの概要と配列ソートの応用例

Ruby

sortについて

sortは配列を昇順でソートするメソッドです。要素の比較は<=>演算子(宇宙船演算子)が広く利用されますが、<=>演算子以外も利用できます。1

sortと<=>を組み合わせた昇順のソート例は以下の通りです。

> (1..10).sort { |a, b| a <=> b }
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

降順の場合は以下の通りです。<=>演算子の左辺と右辺が昇順の場合の逆になります。

> (1..10).sort { |a, b| b <=> a }
=> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

sort_byについて

sort_byはブロックの評価結果を<=>演算子を利用して昇順にソートするメソッドです。

sort_byによる昇順のソート例は以下の通りです。

> (1..10).sort_by { |a| a }
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

降順の場合は以下の通りです。降順の場合は-をつけます。

> (1..10).sort_by { |a| -a }
=> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

sortよりもsort_byを利用する

instance method Enumerable#sort_byでも紹介されている通り、sortは2要素を比較するたびに計算が必要です。一方sort_byにおける計算回数は要素数だけになります。
ですので、パフォーマンスの観点から原則sortよりもsort_byを利用するほうが好ましいです。

sort_by利用時の注意点: Integer以外の降順はうまくソートできない

以下のようにInteger以外の値は降順でうまくソートできません。

> users = [{ name: 'Tom', birth_day: '2001-10-02' }, { name: 'Bob', birth_day: '1981-03-10' }, { name: 'Alice', birth_day: '1971-08-23' }]

### Dateクラスの場合、エラーになってソートができない
> users.sort_by{ |user| -(user[:birth_day].to_date) }
# NoMethodError: undefined method `-@' for Tue, 02 Oct 2001:Date

### Stringクラスの場合、昇順でソートされてしまう(マイナスが機能していない)
> users.sort_by{ |user| -(user[:birth_day]) }
=> [{:name=>"Alice", :birth_day=>"1971-08-23"}, {:name=>"Bob", :birth_day=>"1981-03-10"}, {:name=>"Tom", :birth_day=>"2001-10-02"}]

sort_byでInteger以外を降順にソートする場合はIntegerに変換します。
うまくソートできなかった上記の例をIntegerに変換すると以下のように正しくソートできます。

> users = [{ name: 'Tom', birth_day: '2001-10-02' }, { name: 'Bob', birth_day: '1981-03-10' }, { name: 'Alice', birth_day: '1971-08-23' }]

### Integerに変換すれば降順でソートされる
> users.sort_by{ |user| -(user[:birth_day].to_i) }
=> [{:name=>"Tom", :birth_day=>"2001-10-02"}, {:name=>"Bob", :birth_day=>"1981-03-10"}, {:name=>"Alice", :birth_day=>"1971-08-23"}]

なお、sortの場合はInteger以外の降順も正しくソートできます。

> users = [{ name: 'Tom', birth_day: '2001-10-02' }, { name: 'Bob', birth_day: '1981-03-10' }, { name: 'Alice', birth_day: '1971-08-23' }]

### Dateの場合
> users.sort { |a, b| b[:birth_day].to_date <=> a[:birth_day].to_date }
=> [{:name=>"Tom", :birth_day=>"2001-10-02"}, {:name=>"Bob", :birth_day=>"1981-03-10"}, {:name=>"Alice", :birth_day=>"1971-08-23"}]

### Stringの場合
> users.sort { |a, b| b[:birth_day] <=> a[:birth_day] }
=> [{:name=>"Tom", :birth_day=>"2001-10-02"}, {:name=>"Bob", :birth_day=>"1981-03-10"}, {:name=>"Alice", :birth_day=>"1971-08-23"}]

ソートの応用例

今回はソートの応用例として以下の2つを紹介します。

  • 複合条件でソートをする場合(マルチソート)
  • ソート対象にnilが含まれている場合

複合条件でソートをする場合(マルチソート)

複合条件でソートする場合は配列にソート条件を優先度順で記載します。

「年齢(age)で降順、年齢が同じ場合は給料(salary)で昇順」の例は以下の通りです。

> users = [{ name: 'Tom', age: 20, salary: 300 }, { name: 'Alice', age: 20, salary: 200 }, { name: 'Bob', age: 40, salary: 800 }]

### sort_byの場合
> users.sort_by{ |user| [-user[:age], user[:salary]] }
=> [{:name=>"Bob", :age=>40, :salary=>800}, {:name=>"Alice", :age=>20, :salary=>200}, {:name=>"Tom", :age=>20, :salary=>300}]

### sortの場合
> users.sort { |a, b| [-a[:age], a[:salary]] <=> [-b[:age], b[:salary]] }
=> [{:name=>"Bob", :age=>40, :salary=>800}, {:name=>"Alice", :age=>20, :salary=>200}, {:name=>"Tom", :age=>20, :salary=>300}]

ソート対象にnilが含まれている場合

ソート対象にnilが含まれている場合正常にソートができません。具体例は以下の通りです。

> users = [{ name: 'Tom', age: 20 }, { name: 'Alice', age: nil }, { name: 'Bob', age: 40 }]

### sort_byの場合
> users.sort_by{ |user| user[:age] }
# ArgumentError: comparison of NilClass with 40 failed

### sortの場合
> users.sort { |a, b| a[:age] <=> b[:age] }
# ArgumentError: comparison of Hash with Hash failed

ソート対象にnilが含まれている場合の対処方法について、sort_byとsortそれぞれの紹介をします。

sort_byを利用する場合

OR演算子(||)を利用してnilに対して擬似的な値を設定することでソートが可能になります。

「年齢(age)で降順。ただし、nilは最も小さい値(降順の最後尾)とする」の例は以下の通りです。

> users = [{ name: 'Tom', age: 20 }, { name: 'Alice', age: nil }, { name: 'Bob', age: 40 }]

### nilのageは-10(歳)になるので、nilが最も小さい値として扱われる
> users.sort_by{ |user| -(user[:age] || -10) }

=> [{:name=>"Bob", :age=>40}, {:name=>"Tom", :age=>20}, {:name=>"Alice", :age=>nil}]

sortを利用する場合

<=>演算子の評価値はレシーバー側(左辺)が大きければ1、レシーバー側が小さければ-1です。2
nilを比較した際の<=>演算子の評価値を擬似的に用意することでnilを含んだソートが可能になります。

「年齢(age)で降順。ただし、nilは最も小さい値(降順の最後尾)とする」の例は以下の通りです。

> users = [{ name: 'Tom', age: 20 }, { name: 'Alice', age: nil }, { name: 'Bob', age: 40 }]

users.sort do |a, b|
  if !a[:age]
    # aがnilの場合は <=> の評価を1にする
    # sortは <=> の評価が1の時に[a, b]を[b, a]の順にする。
    # つまり、nilは比較対象の値の後ろにソートされる
    1
  elsif !b[:age]
    # bがnilの場合は <=> の評価を-1にする
    # sortは <=> の評価が-1の時に[a, b]を[a, b]の順にする。(変わらない)
    # つまり、nilは比較対象の値の後ろにソートされる
    -1
  else
    # 降順でソート
    b[:age] <=> a[:age]
  end
end

=> [{:name=>"Bob", :age=>40}, {:name=>"Tom", :age=>20}, {:name=>"Alice", :age=>nil}]

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考記事