Rubyはいろんなことがone lineで書けて便利ですよね。
いろんなことを1行に書けるようにするには
String, Array, Hash, Enumerable
あたりの、リスト系の構造になったものにどういうメソッドがあるかを覚えておけば、たいていのことは1行で書けます。
今回の話は、これに加えて、
のメソッドも知っておくともっと世界が広がるよという話です。
map_with_indexが欲しい!
例えば、あるユーザーのリストに対して、名前の前に通し番号をふった文字列を出力したい(あんまり無さそうな例でごめんなさい)という要求があったとしましょう。
それをただ出力すれば良いのであれば、each_with_index
を使って
users.each_with_index do |user, i|
puts "#{i+1}: #{user.name}"
end
とすれば実現できます。
ですが、この結果が配列として欲しいとなると、map
を使いたいところですが、map_with_index
なんて関数ないしなぁとなります。
これを解決する一つの方法としては、each_with_index
の結果をmap
して
users.each_with_index.map { |user, i| "#{i+1}: #{user.name}" }
とやる方法があります。
でも、これ、カッコ悪いですよね?
で、調べてみると、ありました!Enumeratorの中にwith_index
というメソッドが!
これを使うと、
users.map.with_index { |user, i| "#{i+1}: #{user.name}" }
と書くことができ、とても読みやすくなります。
めでたしめでたし。
ちなみに、with_index
は何の数値からはじめるかを引数にとることができます。なので、
users.map.with_index(1) { |user, i| "#{i}: #{user.name}" }
として、1-originにすることでi+1
と毎回しなくてよくなり、少し綺麗に書くことができます。
with_indexを使って安定ソートを1行で
with_indexにはかなり使える応用があって、それが安定ソート(Stable Sort)です。
何故かRubyはデフォルトで安定ソートが提供されてないんですよね。
例えばFacebookの投稿のようなモデルがあって、それの配列をLike数が多い順、Like数が同じ場合は作られた順でソートしようと思った場合、もしsort_by
がstableであれば以下の様なコードでうまくいくはず。
posts.sort_by(&:created_at).sort_by(&:like_count)
ですが、多分ソートの内部実装がQuick Sortなため、値が等しい時の順番が崩れてしまいます。
posts.sort_by(&:created_at).sort_by.with_index{ |x,i| x.like_count }
と書いて、2回目のソートをstable sort風にすればone lineで書くことができるようになります。
普通にstable_sort_byを使えるようにする
上でもコマンド上でやるときはいいですが、通常は
module Enumerable
def stable_sort_by
self.sort_by.with_index{ |e, index| [yield(e), index] }
end
end
とやった上で、
posts.stable_sort_by(&:created_at).stable_sort_by(&:like_count)
のほうが綺麗ですね。