How to use sort_by to sort by multiple parameters in Ruby

Ruby's #sort_by method is powerful, but it can be difficult to figure out how it works at first, especially when you want to sort by multiple parameters or attributes.

In this sort tutorial, I'm going to show you what you need to know to understand how #sort_by works and how you can use it to sort a collection on multiple attributes.

First, there are a couple of things to understand about how #sort_by works.

Low to High

#sort_by sorts from low to high. So given the following array: [2, 1, 3], [2, 1, 3].sort_by{ |i| i } will return [1, 2, 3].

If you want it sorted the other way, you have two choices:

  1. Negate the operand (if possible)
    [2, 1, 3].sort_by{ |i| -i } will return [3, 2, 1]
  2. Reverse the array
    [2, 1, 3].sort_by{ |i| i }.reverse will return [3, 2, 1]

<=> Operator

Behind the scenes, #sort_by is using the <=> operator to compare each element of the collection. This operator takes two arguments and returns -1 if the first argument is less than the second argument, 1 if the second argument is less than the first argument, or 0 if they're equivalent.

1 <=> 2 #=> -1  
2 <=> 1 #=> 1  
1 <=> 1 #=> 0  

A few other objects in Ruby also respond to the <=> operator.

Date.parse("2015-05-04") <=> Date.parse("2015-05-05") #=> -1  
"a" <=> "b"                                           #=> -1
0.1 <=> 0.2                                           #=> -1  

And if you're creating your own classes, it's easy to define <=> on them:

class Item  
  attr_accessor :quantity

  def <=>(other_item)
    return  0 if self.quantity == other_item.quantity
    return -1 if self.quantity < other_item.quantity
    return  1 if self.quantity > other_item.quantity
  end
end  

Sorting by Multiple Attributes

Let's look at an example of using #sort_by to sort multiple fields of a collection of Tasks in a todo application.

A Task is has:

  • An id
  • A title
  • A due date
  • A priority (integer where 1 is the highest priority)

Let's look at our list of tasks for the week of May 5th, 2015.

Tasks

Id Title Due Date Priority
1 Stop by the Bank 5/5/2015 2
2 Finish TPS Reports 5/8/2015 1
3 Buy groceries 5/4/2015 2
4 Prepare slides for meeting 5/4/2015 1
5 Water the plants 5/6/2015 3
6 Schedule dentist appt 5/4/2015 3

Notice that the current sorting isn't very helpful. By default, the tasks are sorted by Id, or the order in which they were entered into the todo application.

It would make a lot more sense to sort them in some kind of meaningful way. To start, let's sort by tasks that are due sooner, since we'll need to start working on those first.

tasks.sort_by{ |t| t.due_date }  
#or 
tasks.sort_by(&:due_date)  

Tasks

Id Title Due Date Priority
3 Buy groceries 5/4/2015 2
4 Prepare slides for meeting 5/4/2015 1
6 Schedule dentist appt 5/4/2015 3
1 Stop by the Bank 5/5/2015 2
5 Water the plants 5/6/2015 3
2 Finish TPS Reports 5/8/2015 1

This is a great start. Now we can see all of the tasks scheduled for Monday, 5/4 at the top of the list (remember that <=> sorts low to high so the earlier dates come first).

But now we also want to sort the highest priority tasks first, so that we can start working on the most meaningful tasks.

This is where multiple sorting comes in.

tasks.sort_by{ |t| [t.due_date, t.priority] }  

Tasks

Id Title Due Date Priority
4 Prepare slides for meeting 5/4/2015 1
3 Buy groceries 5/4/2015 2
6 Schedule dentist appt 5/4/2015 3
1 Stop by the Bank 5/5/2015 2
5 Water the plants 5/6/2015 3
2 Finish TPS Reports 5/8/2015 1

Now we have tasks sorted first by Due Date and then by Priority. #sort_by handles multiple parameters by sorting the collection according to the first parameter. If there are any ties (in this case, multiple tasks due on Monday 5/4), it will fall back to the next parameter given (in our case, priority).

Once you can picture it, it's a simple but rather powerful mechanism.

In a future post, I will cover some of the other ways to sort information in Ruby (and Rails) and the pros and cons of the different approaches.

Andrew Allen

Author of EfficientRails.com, Software Engineer @Munchery, Former startup founder.

comments powered by Disqus