Enumerable - enum.sort_by {| obj | block } => array
Sorts enum using a set of keys generated by mapping the values in enum through the given block.
%w{ apple pear fig }.sort_by {|word| word.length}
#=> ["fig", "pear", "apple"]
The current implementation of sort_by generates an array of tuples containing the original collection element and the mapped value. This makes sort_by fairly expensive when the keysets are simple
require 'benchmark'
include Benchmark
a = (1..100000).map {rand(100000)}
bm(10) do |b|
b.report("Sort") { a.sort }
b.report("Sort by") { a.sort_by {|a| a} }
end
produces:
user system total real
Sort 0.180000 0.000000 0.180000 ( 0.175469)
Sort by 1.980000 0.040000 2.020000 ( 2.013586)
However, consider the case where comparing the keys is a non-trivial operation. The following code sorts some files on modification time using the basic sort method.
files = Dir["*"]
sorted = files.sort {|a,b| File.new(a).mtime <=> File.new(b).mtime}
sorted #=> ["mon", "tues", "wed", "thurs"]
This sort is inefficient: it generates two new File objects during every comparison. A slightly better technique is to use the Kernel#test method to generate the modification times directly.
files = Dir["*"]
sorted = files.sort { |a,b|
test(?M, a) <=> test(?M, b)
}
sorted #=> ["mon", "tues", "wed", "thurs"]
This still generates many unnecessary Time objects. A more efficient technique is to cache the sort keys (modification times in this case) before the sort. Perl users often call this approach a Schwartzian Transform, after Randal Schwartz. We construct a temporary array, where each element is an array containing our sort key along with the filename. We sort this array, and then extract the filename from the result.
sorted = Dir["*"].collect { |f|
[test(?M, f), f]
}.sort.collect { |f| f[1] }
sorted #=> ["mon", "tues", "wed", "thurs"]
This is exactly what sort_by does internally.
sorted = Dir["*"].sort_by {|f| test(?M, f)}
sorted #=> ["mon", "tues", "wed", "thurs"]
이런 글도 있다.
http://redcorundum.blogspot.com/2006/10/using-sortby-instead-of-sort.html
요약
Enumerable의 sort와 sort_by 는 둘 다 enum객체를 정렬해 주는 일을 한다. 근데 이 둘이 정렬하는 방법이 조금 다르다. sort_by는 정렬 대상이 되는 키들을 일단 한번 연산하여 테이블을 만들어 두고 나서 정렬하기 때문에 간단한 키셋인 경우 sort보다 좀 더 비싸진다.
그런데 sort_by 를 써야 할 상황이 존재한다. sort_by는 초기에 한번 정렬 대상값을 구해놓고 정렬을 하기 때문에 위 설명처럼 File 객체가 2nlogn 개만큼 생성된다든지, Time 객체가 2nlogn 개 만큼 생기거나 하지는 않는다. 이런 경우 sort_by가 sort보다 훨씬 좋은 벤치마크 결과를 내게 된다.
Schwartzian Transform이 멋지다. sort_by가 없었다면 루비스트들은 다 sort에 저 방식으로 정렬을 하고 있겠지? [정렬키, 아이템] 으로 묶은 후(collect) 정렬(sort)해서, 아이템만 취하는(collect) 방식이다. 펄 유저들은 이렇게 짜왔었나보다.
그리고 그 아래 링크는 여러 키를 조건으로 소트하는 방법에 sort_by를 이용하는 글이다. 역시 설명이 참 심플한데, sort에 Array로 인자를 넘기면 각 인자를 비교해 주니, 그걸 이용해서 내가 정렬하고 싶은 순서대로 키를 넘기는거다.
- some_collection.sort do |a, b|
- val = a.foo <=> b.boo
- val = a.bar <=> b.bar if val == 0
- val = a.baz <=> b.baz if val == 0
- val
- end
대신
-
some_collection.sort_by { |a| [a.foo, a.bar, a.baz] }
아하. 그렇구나.
ps. 루비 레퍼런스 설명도 너무 쉽게 잘 돼있어서 감동. ㅜㅜ
이 글은 스프링노트에서 작성되었습니다.