Forum: Ruby Sort array by two attributes? (like sql "order by A, B")

Posted by Max Williams (max-williams)
on 2008-08-11 17:18
IN sql we can pass two arguments to the 'order by' component, and it
will order the results by A, and then B in the cases where A is the
same.

Can anyone think of a way to do the same thing with a ruby array?  For
example, if the array holds objects that have attributes/methods
"lastname" & "firstname", to order the objects in a similar way to the
sql query?

thanks
max
Posted by James Coglan (Guest)
on 2008-08-11 17:30
(Received via mailing list)
>
> Can anyone think of a way to do the same thing with a ruby array?  For
> example, if the array holds objects that have attributes/methods
> "lastname" & "firstname", to order the objects in a similar way to the
> sql query?



Just use Enumerable#sort:
http://ruby-doc.org/core/classes/Enumerable.html#M003150

objects.sort do |a,b|
  comp = (a.lastname <=> b.lastname)
  comp.zero? ? (a.firstname <=> b.firstname) : comp
end

James
http://blog.jcoglan.com
http://github.com/jcoglan
Posted by Jesús Gabriel y Galán (Guest)
on 2008-08-11 17:30
(Received via mailing list)
On Mon, Aug 11, 2008 at 5:16 PM, Max Williams
<toastkid.williams@gmail.com> wrote:
> IN sql we can pass two arguments to the 'order by' component, and it
> will order the results by A, and then B in the cases where A is the
> same.
>
> Can anyone think of a way to do the same thing with a ruby array?  For
> example, if the array holds objects that have attributes/methods
> "lastname" & "firstname", to order the objects in a similar way to the
> sql query?
>

Here's one way: the trick is to create an array with the fields you want
to order by in the sort_by block:

irb(main):001:0> class A
irb(main):002:1> attr_accessor :a,:b
irb(main):003:1> def initialize a,b
irb(main):004:2> @a = a
irb(main):005:2> @b = b
irb(main):006:2> end
irb(main):007:1> end
=> nil
irb(main):008:0> ary = [A.new(1,2), A.new(1,3), A.new(1,1),
A.new(2,3), A.new(2,1)]
=> [#<A:0xb7b68890 @b=2, @a=1>, #<A:0xb7b6887c @b=3, @a=1>,
#<A:0xb7b68868 @b=1, @a=1>, #<A:0xb7b68854 @b=3, @a=2>, #<A:0xb7b68840
@b=1, @a=2>]
irb(main):009:0> ary.sort_by {|x| [x.a,x.b]}
=> [#<A:0xb7b68868 @b=1, @a=1>, #<A:0xb7b68890 @b=2, @a=1>,
#<A:0xb7b6887c @b=3, @a=1>, #<A:0xb7b68840 @b=1, @a=2>, #<A:0xb7b68854
@b=3, @a=2>]

Hope this helps,

Jesus.
Posted by Max Williams (max-williams)
on 2008-08-11 17:34
Jesús Gabriel y Galán wrote:

> Here's one way: the trick is to create an array with the fields you want
> to order by in the sort_by block:
> Hope this helps,
> 
> Jesus.

That's a neat trick, thanks!  I actually like the first one better 
though as you can swap a & b around in each test to get (eg) sorted by 
name ascending and then a date field descending.

Thanks a lot guys!
max
Posted by Erik Veenstra (Guest)
on 2008-08-11 17:51
(Received via mailing list)
> That's a neat trick, thanks!  I actually like the first one better
> though as you can swap a & b around in each test to get (eg) sorted by
> name ascending and then a date field descending.

Just use -x.b:

a.sort_by{|x| [x.name, -x.date]}

Sort_by is much faster than sort with a block.

gegroet,
Erik V.
Posted by James Coglan (Guest)
on 2008-08-11 18:00
(Received via mailing list)
> Sort_by is much faster than sort with a block.


This is often true as sort_by only calls methods on each object once, 
but it
is not universally true -- see
http://ruby-doc.org/core/classes/Enumerable.html#M003151 for more
information, and run some benchmarks for your use case if it's a big 
issue.
Posted by Martin DeMello (Guest)
on 2008-08-11 19:40
(Received via mailing list)
On Mon, Aug 11, 2008 at 8:48 AM, Erik Veenstra <erikveen@gmail.com> 
wrote:
>> That's a neat trick, thanks!  I actually like the first one better
>> though as you can swap a & b around in each test to get (eg) sorted by
>> name ascending and then a date field descending.
>
> Just use -x.b:
>
> a.sort_by{|x| [x.name, -x.date]}

Doesn't always work. I keep this handy:

class RevCmp
    attr_reader :this

    def initialize(obj)
      @this = obj
    end

    def <=>(other)
      other.this <=> @this
    end

    # not delegating anything else because this is explicitly a 
throwaway
    # object used only inside a sort_by block
end

and then you have

  a.sort_by {|x| [x.name, RevCmp.new(x.date)]}

you could even use a top-level method so you can say

  a.sort_by {|x| [x.name, descending(x.date)]}

where descending(x) returns RevCmp.new(x)

You could even mix it into object to get

  a.sort_by {|x| [x.name, x.date._descending_]}

where I use the underscores as a cosmetic way of making it stand out
inside the sort block

martin
Posted by Pit Capitain (Guest)
on 2008-08-11 20:38
(Received via mailing list)
2008/8/11 James Coglan <jcoglan@googlemail.com>:
>
> objects.sort do |a,b|
>  comp = (a.lastname <=> b.lastname)
>  comp.zero? ? (a.firstname <=> b.firstname) : comp
> end

For this usecase there's also Numeric#nonzero?

  objects.sort do |a, b|
    (a.lastname <=> b.lastname).nonzero? ||
    (a.firstname <=> b.firstname)
  end

Regards,
Pit
Posted by Max Williams (max-williams)
on 2008-08-12 10:22
Pit Capitain wrote:

>   objects.sort do |a, b|
>     (a.lastname <=> b.lastname).nonzero? ||
>     (a.firstname <=> b.firstname)
>   end

Wow, this is all great stuff.  thanks folks.
Posted by Jack V. (jack_v)
on 2013-02-18 18:42
I would just create an array containing the parts you want to compare:

 objects.sort { |a,b| [a.lastname, a.firstname] <=> [b.lastname, 
b.firstname] }
Posted by Robert Klemme (robert_k78)
on 2013-02-18 23:02
(Received via mailing list)
On Mon, Feb 18, 2013 at 6:42 PM, Jack V. <lists@ruby-forum.com> wrote:
> I would just create an array containing the parts you want to compare:
>
>  objects.sort { |a,b| [a.lastname, a.firstname] <=> [b.lastname,
> b.firstname] }

In theory this is less efficient since there are two Array instances
created per comparison.  If you go for the Array solution, using
#sort_by is probably better:

objects.sort_by {|a| [a.lastname, a.firstname]}

Kind regards

robert
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.