From: Steve Howell Date: 2010-10-15T00:35:12+09:00 Subject: Re: sort_by: multiple fields with reverse sort On Oct 14, 8:00 am, Steve Howell wrote: > On Oct 13, 12:46 am, Rahul Kumar wrote: > > > > > I need to use *sort_by* to sort a table, since the user could select > > columns in any order. > > >     b=[ ["radio", 30, 5], ["radio", 20, 5], ["archie", 20, 5], > > ["newton", 10, 3] ] > >     b.sort_by{|x| [ x[1], x[0] ]} > > > Works fine. However, often the user will want a reverse sort on some > > field. I do not off-hand know the datatype of the field, but in most > > cases it will be a String. > > > I tried: > > >     b.sort_by{|x| [ !x[1] ]} > > This works (one criteria), but this does not: > >     b.sort_by{|x| [ !x[1], x[0] ]} > > > Another thread here, suggested using a minus for Numbers but what about > > Strings. > > Here is a solution that allows you to lazily evaluate keys and specify > that certain keys are to be reversed. > >   module Enumerable >     def sort_by(*key_methods) >       # allow for multiple key_methods and only >       # evaluate them when they are truly needed >       # for the sort >       def compare(a,b, key_methods) >         i = 0 >         while i < key_methods.size do >           for elem in [a, b] do >             fields = elem[0] >             key = elem[1] >             if fields.size <= i >               fields[i] = key_methods[i][0].call(key) >             end >           end >           if key_methods[i][1] == :desc >             a, b = b, a >           end >           result = (a[0][i] <=> b[0][i]) >           return result unless result == 0 >           i += 1 >         end >         return result >       end > >       self.collect do |item| >         [ [], item ] >       end.sort do |a, b| >         compare(a, b, key_methods) >       end.collect do |kv| >         kv[1] >       end >     end >   end > >   a = [ >     ["radio", 30, 5], >     ["radio", 40, 5], >     ["radio", 20, 5], >     ["archie", 20, 5], >     ["newton", 10, 3] >   ] > >   puts a.sort_by( >     [Proc.new { |a| a[0] }], >     [Proc.new { |a| a[1] }, :desc] >   ).inspect Oops, I forgot to verify that I was actually caching results. Here is a fixed version. module Enumerable def sort_by_multi(*key_methods) # allow for multiple key_methods and only # evaluate them when they are truly needed # for the sort def compare(a,b, key_methods) i = 0 while i < key_methods.size do for elem in [a, b] do key = elem[1] if elem[0].size <= i elem[0] << key_methods[i][0].call(key) puts " #{elem.inspect}" end end if key_methods[i][1] == :desc a, b = b, a end result = (a[0][i] <=> b[0][i]) return result unless result == 0 i += 1 end return result end self.collect do |item| [ [], item ] end.sort do |a, b| compare(a, b, key_methods) end.collect do |kv| kv[1] end end end a = [ ["radio", 30, 5], ["radio", 40, 5], ["radio", 20, 5], ["archie", 20, 5], ["newton", 10, 3] ] puts a.sort_by_multi( [Proc.new { |a| a[0] }], [Proc.new { |a| a[1] }, :desc] ).inspect You can see that it only evaluates the second key for tiebreaker purposes. (Of course, in this example, the key calculation is trivial, but in the real world you might have a key that is expensive to evaluate.) [["radio"], ["radio", 30, 5]] [["radio"], ["radio", 20, 5]] [["radio", 30], ["radio", 30, 5]] [["radio", 20], ["radio", 20, 5]] [["newton"], ["newton", 10, 3]] [["radio"], ["radio", 40, 5]] [["radio", 40], ["radio", 40, 5]] [["archie"], ["archie", 20, 5]] [["archie", 20, 5], ["newton", 10, 3], ["radio", 40, 5], ["radio", 30, 5], ["radio", 20, 5]]