Rubyで配列をちょっとした条件でソートする


ある配列を独自の条件でソートするときもsort_byを使うとたいへん便利なのですが、書き方を失敗したときのエラーがわかりにくい場合があったので記事を書いてみます(5年くらい前もあれ、となっていたので)。

> (1..10).to_a.reverse
=> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

たとえばこんな配列があります。これを昇順にソートするには単純にsortを使えばよい。

> (1..10).to_a.reverse.sort
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

これを偶数を先に並べるにはどうしましょう。
sort_byをつかうと簡単です。

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

偶数が先だけれど、昇順にしたいときは、sort_byのなかを配列にするとできます。

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

数字か文字列かで違うこともあまりないんですが、文字列で特定の文字を含む場合に先に並べたいときはどうでしょうか。同じようにやってみると・・・

>  ["さばねこ","いぬ","しばいぬ","ねこ","とらねこ","あきたいぬ" ]
.sort_by{|animal| animal.include?("いぬ")} 
ArgumentError: comparison of Array with Array failed

エラーが出ました。
配列と配列を比べていると失敗しています。ちょっとやっていることとわかりにくく、うまく説明できませんが、TrueやFalseといったBoolean同士を比較できないから、ではないかと思います(どなたかいい説明あれば教えてください・・・)

原因はさておき、どう解決するか。三項間演算子で数字にするのが簡単です。

> ["さばねこ","いぬ","しばいぬ","ねこ","とらねこ","あきたいぬ" ]
.sort_by{|animal| [animal.include?("いぬ") ? 0 : 1]}
=> ["いぬ", "しばいぬ", "あきたいぬ", "さばねこ", "ねこ", "とらねこ"]

いぬが先でそのなかで辞書順にするにはこうします

> ["さばねこ","いぬ","しばいぬ","ねこ","とらねこ","あきたいぬ" ]
.sort_by{|animal| [animal.include?("いぬ") ? 0 : 1, animal]}
=> ["あきたいぬ", "いぬ", "しばいぬ", "さばねこ", "とらねこ", "ねこ"]

はい。
簡単でした。多次元配列やHashの配列であっても同じような考え方でsort_byを使うと簡単です。配列に列挙していくのでなにを優先させるかもわかりやすいですね。

参考)
sort_by のドキュメントはこちら。
https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/sort_by.html