【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