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

73c04e9ef9ca435c5b19a2e765ae6d20?d=identicon&s=25 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
53581739a445ad78250a676dabddf55f?d=identicon&s=25 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
E088bb5c80fd3c4fd02c2020cdacbaf0?d=identicon&s=25 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.
73c04e9ef9ca435c5b19a2e765ae6d20?d=identicon&s=25 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
6b0967f63d03e99b6c07a3f5ed224c77?d=identicon&s=25 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.
53581739a445ad78250a676dabddf55f?d=identicon&s=25 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.
Ae16cb4f6d78e485b04ce1e821592ae5?d=identicon&s=25 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
50b2daf0e7666574579b9edaf8f2b69a?d=identicon&s=25 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
73c04e9ef9ca435c5b19a2e765ae6d20?d=identicon&s=25 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.
B99708c5a96497dd952ad6d24c4da74c?d=identicon&s=25 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] }
E0d864d9677f3c1482a20152b7cac0e2?d=identicon&s=25 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
4828d528e2e46f7c8160c336eb332836?d=identicon&s=25 Robert Heiler (shevegen)
on 2014-08-13 14:05
Can someone explain to me why it works?


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


Why does this kind of sorting work on an Array input?

How would I know that it first starts by the first
Array member, and then the second? Why is it not
the other way (from last to first, rather from first
to last entry)?
54404bcac0f45bf1c8e8b827cd9bb709?d=identicon&s=25 7stud -- (7stud)
on 2014-08-13 18:28
Robert Heiler wrote in post #1155030:
> Can someone explain to me why it works?
>
>
>   array.sort_by{|x| [x.name, -x.date] }
>
>
> Why does this kind of sorting work on an Array input?
>

When an Array calls a method, the block specified after
the method call does not have to take an array as
an argument.  Here is an example:

class Array
  def do_stuff

    if block_given?
      map do |array_elmt|
        yield array_elmt
      end
    else
      self
    end

  end
end

data = [10, 20, 30]

y = data.do_stuff { |x| x*2 }
p y

y = data.do_stuff
p y

--output:--
[20, 40, 60]
[10, 20, 30]


Enumerable#sort_by() uses a block in a similar fashion.
sort_by() sends each
element of an Array that you want to sort to the block to create a new
value that it will use
as a stand in for the original value when sorting.  The return values
from the block are what sort_by() uses as the values to sort.  For
example, look at this Hash:

mapped_orig_vals = {

    ['David', 2] => objA
    ['David', 1] => objB,

}

After creating each entry in the hash, sort_by() sorts the keys, which
are arrays, to produce:

ordered_keys = [

  ['David', 1],
  ['David', 2],

]

After sorting the keys, sort_by() looks up the value
associated with each key to get the original value, e.g.

ordered_orig_vals = ordered_keys.map do |key|
  mapped_orig_values[key]
end
#=>[objB, objA]

> How would I know that it first starts by the first
> Array member, and then the second?

If by Array you mean the Array returned by the sort_by() block, you
would know by reading about how ruby determines whether two
arrays are equal or not:

http://www.ruby-doc.org/core-2.1.2/Array.html#meth...

In every computer programming language I've studied, things like Arrays
and Strings are always compared by starting with the first
element/character, and if they are the same, then the second element is
compared, etc.  Once a difference is found, then one Array/String is
considered smaller than the other Array/String.  If all the elements are
the same but one Array/String is shorter than the other, the shorter one
is considered smaller.  If all the elements are the same, and they are
the same length, then they are considered equal.

On the other hand, if by Array you mean the original Array you are
trying to sort, it is irrelevant which end of the array you start at.
30e0434c99d6ddbfcaa733288790de7b?d=identicon&s=25 Maggie Davis (withful)
on 2014-08-15 11:36
Just use Enumerable#sort: http://www.ruby-doc.org
and you know this video http://youtu.be/1DzlYY4saMY
Please log in before posting. Registration is free and takes only a minute.
Existing account

NEW: Do you have a Google/GoogleMail, Yahoo or Facebook account? No registration required!
Log in with Google account | Log in with Yahoo account | Log in with Facebook account
No account? Register here.