How to order Structs based on two fields

Hi, I’ve a struct like this:

KK = Struct.new(:a,:b)

and some entries in an array:

kk1=KK.new(0,10)
#

kk2=KK.new(0,5)

kk3=KK.new(2,0)
#

kk4 = KK.new(2,5)
#

array = [kk3, kk2, kk1, kk4]

I need to order the array based:

  • Elements with minor :a must be first.
  • If two elements have same :a, then order based on higher :b.

The result should be:

[kk1, kk2, kk4, kk3]

I expected that the following could work:

array.sort_by{|entry| entry.a or entry.b}

but it generates:

[kk1, kk2, kk3, kk4]

so obviously it does not work. I’m not very used to Enumerable.sort_by
method, any tip please?

Thanks a lot.

2011/7/1 Iñaki Baz C. [email protected]:

I expected that the following could work:

array.sort_by{|entry| entry.a or entry.b}

but it generates:

[kk1, kk2, kk3, kk4]

so obviously it does not work. I’m not very used to Enumerable.sort_by
method, any tip please?

Maybe it would be better to use “class KK” rather than an struct, and
then define <=> method for KK class (and include Enumerable module)?

Try this: array.sort { |x,y| x.a == y.a ? x.b <=> y.b : x.a <=> y.a }
Or overide the <=> method in your struct.

2011/7/1 Iaki Baz C. [email protected]

On Fri, Jul 1, 2011 at 5:55 AM, Iaki Baz C. [email protected] wrote:

method, any tip please?

Maybe it would be better to use “class KK” rather than an struct, and
then define <=> method for KK class (and include Enumerable module)?


Iaki Baz C.
[email protected]

Structs return classes already (this is why you can inherit from them in
the
form class A < Struct.new(:a,:b)). You can pass Struct.new a block,
and it
will be evaluated within the context of the struct you are creating:

KK = Struct.new(:a,:b) do
def <=>(kk)
if a < kk.a
-1
elsif a > kk.a
1
else
kk.b <=> b
end
end
end

kk1=KK.new(0,10)
kk2=KK.new(0,5)
kk3=KK.new(2,0)
kk4 = KK.new(2,5)
array = [kk3, kk2, kk1, kk4]

Elements with minor :a must be first.

If two elements have same :a, then order based on higher :b.

The result should be:

[kk1, kk2, kk4, kk3] == array.sort # => true

Sorry code must be array.sort { |x,y| x.a == y.a ? -(x.b <=> y.b) : x.a
<=>
y.a }

2011/7/1 Gunther D. [email protected]

On Fri, Jul 1, 2011 at 4:22 PM, Iaki Baz C. [email protected] wrote:

I need to order the array based: array.sort_by{|entry| entry.a or entry.b}

How about:

array.sort do |i, j|
r = i.a <=> j.a
case r
when 0 r
else -1 * (i.b <=> j.b)
end
end

2011/7/1 Josh C. [email protected]:

 1

else
kk.b <=> b
end
end
end

Really thanks to all for your responses, I will try them :slight_smile:

2011/7/1 Robert K. [email protected]:

array.sort_by {|e| [e.a, e.b]}

Note that entries must be ordered by smaller :a and, in case of same
:a, by higher :b. So the last “e.b” should be changed to something
else. Trying it right now.

Thanks a lot.

On Jul 1, 2011, at 12:55 PM, Iaki Baz C. wrote:

method, any tip please?

Maybe it would be better to use “class KK” rather than an struct, and
then define <=> method for KK class (and include Enumerable module)?

You can have both:

KK = Struct.new(:a, :b) do
include Enumerable

def <=>
#…
end
end

Regards,
Florian

On Fri, Jul 1, 2011 at 12:55 PM, Iaki Baz C. [email protected]
wrote:

method, any tip please?

Maybe it would be better to use “class KK” rather than an struct, and
then define <=> method for KK class (and include Enumerable module)?

You can do

array.sort_by {|e| [e.a, e.b]}

But you can of course define <=> for a Struct class as well

KK = Struct.new :a,:b do
include Comparable

def <=>(o)
cmp = a <=> o.a
cmp == 0 ? b <=> o.b : cmp
end
end

Kind regards

robert

You can do

array.sort_by {|e| [e.a, e.b]}

Nice. Little mistake (same as mine). Code should be array.sort_by { |e|
[e.a, -e.b] }

On Fri, Jul 1, 2011 at 6:30 AM, Iaki Baz C. [email protected] wrote:

Really thanks to all, this community is really impresive.


Iaki Baz C.
[email protected]

I recommend selecting the most straightforward one. Are there any
solutions
where you look at them and they are so obvious as to seem mundane? Later
when you look at this code, you’ll be appreciative of code like that.

2011/7/1 Gunther D. [email protected]:

Nice. Little mistake (same as mine). Code should be array.sort_by { |e|
[e.a, -e.b] }

Yeah :slight_smile:

So I’ve lot of solutions right now, and all of them work :slight_smile:

I’ll do some benchmarks and select the fastest one.

Really thanks to all, this community is really impresive.

On Fri, Jul 1, 2011 at 1:22 PM, Iaki Baz C. [email protected] wrote:

2011/7/1 Robert K. [email protected]:

array.sort_by {|e| [e.a, e.b]}

Note that entries must be ordered by smaller :a and, in case of same
:a, by higher :b. So the last “e.b” should be changed to something
else. Trying it right now.

Oh, my mistake. I am sorry. So that would be (for numeric b):

array.sort_by {|e| [e.a, -e.b]}

And then

KK = Struct.new :a,:b do
include Comparable

def <=>(o)
cmp = a <=> o.a
cmp == 0 ? o.b <=> b : cmp
end
end

Cheers

robert

2011/7/1 Josh C. [email protected]:

I recommend selecting the most straightforward one. Are there any solutions
where you look at them and they are so obvious as to seem mundane? Later
when you look at this code, you’ll be appreciative of code like that.

Sure, I’ll also consider it :wink:

On Jul 1, 2011, at 7:18 AM, Robert K. wrote:

But you can of course define <=> for a Struct class as well

KK = Struct.new :a,:b do
include Comparable

def <=>(o)
cmp = a <=> o.a
cmp == 0 ? b <=> o.b : cmp
end
end

Actually, there’s a method to help with exactly this sort of thing
(pun intended).

def <=>(o)
(a <=> o.a).nonzero? || b <=> o.b
end

Take a look at Numeric#nonzero? and you’ll find an example that’s
almost exactly this.

-Rob

Kind regards

robert


remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Rob B.
[email protected] http://AgileConsultingLLC.com/
[email protected] http://GaslightSoftware.com/

2011/7/1 Iñaki Baz C. [email protected]:

def <=>(o)
(a <=> o.a).nonzero? || b <=> o.b
end

Take a look at Numeric#nonzero? and you’ll find an example that’s almost
exactly this.

Very cool :slight_smile:

But it would be:

def <=>(o)
(a <=> o.a).nonzero? || o.b <=> b
end

:slight_smile:

2011/7/1 Rob B. [email protected]:

Actually, there’s a method to help with exactly this sort of thing (pun
intended).

def <=>(o)
(a <=> o.a).nonzero? || b <=> o.b
end

Take a look at Numeric#nonzero? and you’ll find an example that’s almost
exactly this.

Very cool :slight_smile:

On 7/1/2011 7:30 AM, Iñaki Baz C. wrote:

I’ll do some benchmarks and select the fastest one.

The efficiency depends on how expensive it is to compute the key of the
elements. If the computation is non-trivial, then Array#sort_by would be
much faster because it caches the keys before the sort so that no
unnecessary computation is done (i.e., the key of each element is
computed exactly once).

If, like it is in your case, the computation only involves several
Fixnum comparisons, Array#sort_by will most likely be slower as it needs
to allocate (cache) the key set prior to the actual sorting.

For example, there should be a difference if the accessors themselves
are expensive. Something like:

def a
sleep 0.01
@a
end

As a side note, this idiom used by Array#sort_by is known as the
Schwartzian transform in the Perl world.

2011/7/1 Su Zhang [email protected]:

For example, there should be a difference if the accessors themselves are
expensive. Something like:

def a
sleep 0.01
@a
end

Thanks. However in my case the class is a Struct and I add no
complexity to accessor. In fact I just use:

KK = Struct.new(:a, :b)
kk1 = KK.new(10,20)