Forum: Ruby how to vary sort's block?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
B7f73e9f9d35be6a44b15f603f2910d4?d=identicon&s=25 Wybo Dekker (Guest)
on 2006-02-04 11:40
(Received via mailing list)
When I have an array of objects (Thing's, say) with two properties (say
`name' and `amount') I want to be able to sort that array by either of
these properties, depending on circumstances (like the value of the
variable `order').

The script below is an example, and it works, but I don't like it.
I there a better way to do it, without an if..else construction?

For example, would it be possible to put several {|a,b|...} blocks for
the sort in a hash with keys telling what I want to sort on?

#!/usr/bin/ruby

class Thing
  attr_reader :name,:amount

  def initialize(name,amount)
    @name,@amount = name,amount
  end

  def list
    puts ['',@name,@amount].join("\t")
  end
end

class Array
  def byamount
    self.sort { |a,b| a.amount <=> b.amount }
  end
  def byname
    self.sort { |a,b| a.name <=> b.name }
  end
end

arr = [
  Thing.new('John',10),
  Thing.new('Anny',20)
]

order = :byname

puts "sorted #{order}:"
if order == :byname
  arr.sort { |a,b| a.name <=> b.name }.each { |a|
      a.list
      # much more code may occur here...
    }
elsif order == :byamount
  arr.sort { |a,b| a.amount <=> b.amount }.each { |a|
      a.list
      # much more code may occur here...
    }
else
  raise "illegal order"
end
5befe95e6648daec3dd5728cd36602d0?d=identicon&s=25 Robert Klemme (Guest)
on 2006-02-04 13:55
(Received via mailing list)
Wybo Dekker <wybo@servalys.nl> wrote:
>
>    puts ['',@name,@amount].join("\t")
> end
>  arr.sort { |a,b| a.name <=> b.name }.each { |a|
> end
The easiest is probably to use sort_by.

Thing = Struct.new(:name, :amount)
arr = [
 Thing.new('John',10),
 Thing.new('Anny',20),
]

p arr.sort_by {|x| x.name}
p arr.sort_by {|x| x.amount}

Btw, I would *not* put byamount and byname into Array.  If you want to
do
something general, then I'd do this:

module Enumerable
  def sort_field(*fields)
    sort_by {|x| fields.map {|f| x.send f}}
  end
end

Then you can do

p arr.sort_field :name
p arr.sort_field :amount

It might even be reasonable to replace the current implementation of
sort_by
with one that accepts either a list of symbols as arguments or a block.
I
think this might have been proposed already.  Matz, any comment on this?
Does this sound reasonable?

Kind regards

    robert
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-02-04 14:17
(Received via mailing list)
On Sat, 2006-02-04 at 19:40 +0900, Wybo Dekker wrote:
>
Yes, see below:

>     puts ['',@name,@amount].join("\t")
> end
>
> arr = [
>   Thing.new('John',10),
>   Thing.new('Anny',20)
> ]

# Replace rest of the code with:
orders = {
           :byname => lambda { |a,b| a.name <=> b.name },
           :byamount => lambda { |a,b| a.amount <=> b.amount }
         }

order = :byname

if blk = orders[order]
  puts "sorted #{order}:"
  arr.sort(&blk).each { |a| a.list }
else
  raise "illegal order"
end

__END__

This would be my first implementation I think, I like dispatch table
type stuff. Be careful what you enclose with all those blocks, though.

An alternative idea might be (replacing the code I show above):

	require 'curry'   # Have to download this, see bottom

	sort_proc = lambda { |msg,a,b| a.send(msg) <=> b.send(msg) }

	by_name = sort_proc.curry(:name)
	by_amount = sort_proc.curry(:amount)

	puts "sorted name:"
	arr.sort(&by_name).each { |a| a.list }
>>	# => sorted name:
>>	# =>         Anny    20
>>	# =>         John    10

	puts "sorted amount:"
	arr.sort(&by_amount).each { |a| a.list }
>>	# => sorted amount:
>>	# =>         John    10
>>	# =>         Anny    20


(You could curry on the fly of course depending on the sort criteria,
it's reasonably low-overhead).

Does it show I've been searching for an opportunity to use currying
since I did that quiz entry? ;D

(Curry: http://www.ruby-talk.org/cgi-bin/scat.rb/ruby/ruby... )
B7f73e9f9d35be6a44b15f603f2910d4?d=identicon&s=25 Wybo Dekker (Guest)
on 2006-02-04 15:48
(Received via mailing list)
On Sat, 4 Feb 2006, Robert Klemme wrote:

> The easiest is probably to use sort_by.
>
> Thing = Struct.new(:name, :amount)
> arr = [
> Thing.new('John',10),
> Thing.new('Anny',20),
> ]
>
> p arr.sort_by {|x| x.name}
> p arr.sort_by {|x| x.amount}

this does not allow for more complex sorts, like on name *and* amount,
does it?

>
> p arr.sort_field :name
> p arr.sort_field :amount

but this is really great, I can now even say:
   p arr.sort_field(:name,:amount)
Thanks!

> It might even be reasonable to replace the current implementation of sort_by
> with one that accepts either a list of symbols as arguments or a block.  I
> think this might have been proposed already.  Matz, any comment on this? Does
> this sound reasonable?

I saw that it is also possible to give sort an argument instead of a
block:

require 'pp'

Thing = Struct.new(:name, :amount)
arr = [
  Thing.new('John',80),
  Thing.new('John',10),
  Thing.new('John',30),
  Thing.new('Anny',20),
  Thing.new('Anny',10),
  Thing.new('Anny',30),
  Thing.new('Anny',15)
]

def byname(a,b)     a.name <=> b.name end
def byamount(a,b) a.amount <=> b.amount end
def by_name_amount(a,b)
  (a.name <=> b.name)*4 + (a.amount <=> b.amount)
end

[:byname,:byamount,:by_name_amount].each do |order|
  puts "sort #{order}:"
  pp arr.sort(&method(order))
end

But ri does not tell me that:

-------------------------------------------------------- Enumerable#sort
     enum.sort                     => array
     enum.sort {| a, b | block }   => array
------------------------------------------------------------------------
     Returns an array containing the items in _enum_ sorted, either
     according to their own +<=>+ method, or by using the results of the
     supplied block. The block should return -1, 0, or +1 depending on
     the comparison between _a_ and _b_. As of Ruby 1.8, the method
     +Enumerable#sort_by+ implements a built-in Schwartzian Transform,
     useful when key computation or comparison is expensive..

        %w(rhea kea flea).sort         #=> ["flea", "kea", "rhea"]
        (1..10).sort {|a,b| b <=> a}   #=> [10, 9, 8, 7, 6, 5, 4, 3, 2,
1]
5befe95e6648daec3dd5728cd36602d0?d=identicon&s=25 Robert Klemme (Guest)
on 2006-02-04 18:21
(Received via mailing list)
"Wybo Dekker" <wybo@servalys.nl> schrieb im Newsbeitrag
news:Pine.LNX.4.61.0602041523170.10213@servalys.nl...
>> p arr.sort_by {|x| x.name}
>> p arr.sort_by {|x| x.amount}
>
> this does not allow for more complex sorts, like on name *and* amount,
> does it?

It does: arr.sort_by {|x| [x.name, x.amount]} (depending on the
precedence
you want).  I believe, if you use Struct and want to order according to
field order then you might be able to just use sort() without arguments
(alternatively sort_by {|x| x.to_a} - to_a is defined by Struct).

>  Thing.new('Anny',20),
>
> ------------------------------------------------------------------------
>     Returns an array containing the items in _enum_ sorted, either
>     according to their own +<=>+ method, or by using the results of the
>     supplied block. The block should return -1, 0, or +1 depending on
>     the comparison between _a_ and _b_. As of Ruby 1.8, the method
>     +Enumerable#sort_by+ implements a built-in Schwartzian Transform,
>     useful when key computation or comparison is expensive..
>
>        %w(rhea kea flea).sort         #=> ["flea", "kea", "rhea"]
>        (1..10).sort {|a,b| b <=> a}   #=> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Actually ri tells you that - because it's the block form.  Your methods
are
implicitely converted to blocks.  In that case I'd rather not use a
method
definition but rather do the more direct lambda / proc:

Thing::BYNAME = lambda {|a,b| a.name <=> b.name}
Thing::BYAMOUNT = lambda {|a,b| a.amount <=> b.amount}
....
arr.sort(&Thing::BYNAME)

You can as well define those constants in global scope but I felt they
are
more appropriately placed in Thing's scope because they refer exactly to
Things.

Kind regards

    robert
4299e35bacef054df40583da2d51edea?d=identicon&s=25 James Gray (bbazzarrakk)
on 2006-02-04 18:27
(Received via mailing list)
On Feb 4, 2006, at 4:40 AM, Wybo Dekker wrote:

> When I have an array of objects (Thing's, say) with two properties
> (say
> `name' and `amount') I want to be able to sort that array by either of
> these properties, depending on circumstances (like the value of the
> variable `order').

I would use send():

#!/usr/bin/ruby

class Thing
   attr_reader :name,:amount

   def initialize(name,amount)
     @name,@amount = name,amount
   end

   def list
     puts ['',@name,@amount].join("\t")
   end
end

arr = [
   Thing.new('John',10),
   Thing.new('Anny',20)
]

order = :name

puts "sorted by #{order}:"
arr.sort_by { |thing| thing.send(order) }.each { |a|
     a.list
     # much more code may occur here...
} rescue puts("illegal order")

__END__

Hope that helps.

James Edward Gray II
B7f73e9f9d35be6a44b15f603f2910d4?d=identicon&s=25 Wybo Dekker (Guest)
on 2006-02-05 14:19
(Received via mailing list)
Thanks all for the very valuable explanations and suggestions.
I finally came to the conclusion that, for my problem (to do a sort,
depending on the value of some variable `order'), sort_by is the most
versatile way. I saved an example for future reference, which may be
useful for others on the list:

#!/usr/bin/ruby

class Thing
  attr_reader :name,:amount
  def initialize(name,amount)
    @name,@amount = name,amount
  end
  def list
    puts ['',@name,@amount].join("\t")
  end
end

arr = [
  Thing.new('Johnny',20), Thing.new('Anny',20),
  Thing.new('John',10),   Thing.new('Anny',10),
  Thing.new('John',30),   Thing.new('Anny',30),
]

for order in [:name, :amount]
  puts "by #{order}:"
  arr.sort_by{|x| x.send(order)}.each {|a| a.list}
end

order = :name
puts "by #{order}_length, #{order}, amount:"
arr.sort_by{|x| [ x.send(order).length,
                  x.send(order),
                  x.amount
                ]
}.each {|a| a.list}

PS. Good to see in the definition of arr that the extra comma after the
last element is not an error anymore.
48641c4be1fbe167929fb16c9fd94990?d=identicon&s=25 Mark Wilden (Guest)
on 2006-02-06 01:51
(Received via mailing list)
"Wybo Dekker" <wybo@servalys.nl> wrote in message
news:Pine.LNX.4.61.0602051338380.10213@servalys.nl...

> PS. Good to see in the definition of arr that the extra comma after the
> last element is not an error anymore.

Agreed. C# is the same.
This topic is locked and can not be replied to.