【Ruby】sortメソッドでマルチソートする方法

やりたいこと

  • sortのブロック内で複数キーを用いたマルチソートを実行したい
  • 項目にnilを含む場合は、nilの項目を末尾に追いやりたい
  • その上で特定のキーで昇順、降順を制御したい

sortの挙動

ブロックとともに呼び出された時には、要素同士の比較をブロックを用いて行います。ブロックに2つの要素を引数として与えて評価し、その結果で比較します。ブロックは <=> 演算子と同様に整数を返すことが期待されています。つまり、ブロックは第1引数が大きいなら正の整数、両者が等しいなら0、そして第1引数の方が小さいなら負の整数を返さなければいけません。両者を比較できない時は nil を返します。

引用:Array#sort (Ruby 3.1 リファレンスマニュアル)

Arrayの<=>メソッド

自身と other の各要素をそれぞれ順に <=> で比較していき、結果が 0 でなかった場合にその値を返します。各要素が等しく、配列の長さも等しい場合には 0 を返します。各要素が等しいまま一方だけ配列の末尾に達した時、自身の方が短ければ -1 をそうでなければ 1 を返します。 other に配列以外のオブジェクトを指定した場合は nil を返します。

引用:Array# (Ruby 3.1 リファレンスマニュアル)

  • sortのブロック内で配列同士の比較もできる。
  • 配列の先頭から比較して同値の場合には次の要素を検証していく
  • 同値じゃなければ1または-1を返す

idの降順でソート

  • 降順でのソートの場合はsortにブロックを渡して、二つ目のブロック変数を先にしてあげることで再現可能
customer.action_histories.sort |a, b| do 
	b.id <=> a.id
end

nameの昇順、idの降順でソート

  • 第一ソートキーであるnameで比較、そこで同値じゃなければ第二ソートキーは評価せず、右辺の大小に応じて並べ替える
customer.action_histories.sort |a, b| do 
	[a.name, b.id] <=> [b.name, a.id]
end

日付(Date)の降順でソート

  • Dateクラスにも<=>メソッドが実装されているため考え方は配列と同じ
customer.action_histories.sort |a, b| do 
	b.action_datetime <=> a.action_datetime
end

ソート対象にnilが含まれていて、nilの項目は末尾にソート

  • nilが含まれている属性に対してnil?を実行してtrue/falseに応じて0か1を返す
  • nilの場合は0が返されて、それ以外では1が返される。降順でのソートのため0が返されたnilの項目は末尾に回される
  • その後idがnilじゃないもの同士は1で同値となるので、第二ソートキーの昇順で並べ替えが行われる
customer.action_histories.sort |a, b| do 
	[b.id.nil? ? 0 : 1, a.action_datetime] <=> [a.id.nil? ? 0 : 1, b.action_datetime]
end