views:

571

answers:

4

I have a list of Fruit structs called basket. Each Fruit struct has a name (a string) and a calories (an integer). I would like to sort basket so that:

  1. The Fruits with the highest calories appear first. For example, a fruit with 500 calories appears before a fruit with 400 calories.

  2. If two Fruits have equal calories, the Fruit whose name comes first alphabetically comes first, ignoring case. For example, given two fruits with equal calories, one named "banana" will come before one named "Citrus".

The definition of Fruit is not something I control so I'd prefer a solution which doesn't involve mixing anything into Fruit or changing it. Is this possible?

+1  A: 

See Array#sort (API doc). You can pass in a block that returns -1, 0, or 1 given two Fruit objects, and your block can determine these values using whatever attributes you please.

Marc W
The problem with 'sort' is that it re-evaluates the block multiple times, for each pair of items that are compared. using 'sort_by' calculates the comparison key *once* for each item and sorts those
Gareth
..although having said that, it's unlikely to be an issue in this case, as this example won't be using a slow calculation. But still, I think that sort_by is generally a more scalable solution
Gareth
+7  A: 

The easy solution is

basket.sort_by { |f| [-f.calories, f.name] }

Of course, if this is the canonical sort order for fruit then it should be defined using the <=> method and with the Comparable module mixed into Fruit

Gareth
Does this take the fact that f.name should be compared case-insensitively into account?
Kyle Kaitan
to do case ignore, swap in f.name.downcase
rampion
This plus a f.name.downcase was exactly what I needed. Thanks! (No, there isn't a defined canonical sort order for fruit; it just so happens that this is how it needs to be sorted in this particular case.)
Kyle Kaitan
+1  A: 

Let's assume that your basket is an Array or a subclass thereof.

The Fast Way

Enumerable.sort_by

As Gareth pointed out, Enumerable (included by Array) has a sort_by method that runs through each list item once. This is faster to run and faster to write once you get the hang of it.

# -f.calories to sort descending
# name.downcase to do a case-insensitive sort
basket = basket.sort_by { |f| [-f.calories, f.name.downcase] }

The Perl Way

Array.sort

Coming from a Perl background, my first impulse is to grab the spaceship operator <=>. Cheeky little devil. Array has the sort and sort! methods that make it very useful. This solution is slower, and because it's longer it is more likely to introduce bugs. The only reason to use it is if you're dealing with people unfamiliar with Ruby and unwilling to find the right way on StackOverflow.

baseket.sort! { |a,b|
  if a.calories == b.calories
    a.name.downcase <=> b.name.downcase
  else
    # Reverse the result to sort highest first.
    -(a.calories <=> b.calories)
  end
}
Adrian Dunston
A: 

If you need to sort Fruits a lot, you should probably do a little more work up front and make your objects comparable.

For this, you need to implment the Spaceship-Operator (<=>) and include Comparable.

class Fruit
  attr_accessor :name, :color

  def <=>(other)
    # use Array#<=> to compare the attributes
    [self.name.downcase, self.color] <=> [other.name.downcase, other.color]
  end

  include Comparable
end

then you can simply do:

list_of_fruits.sort

Comparable also gives you many other methods (==, <, >) for free, so you can do things like if (apple < banana) (see the documentation for the Comparable Module for more info)

<=>, is specified to return -1 if self is smaller than other, +1 if other is smaller and 0 if both objects are equal.

levinalex