Q:

Understanding Ruby sort_by

I am new to ruby and I struggle understanding sort_by!. This method does something magically I really don't understand.
Here is a simple example:
pc= ["Z6","Z5","Z4"]

c = [
    {
        id: "Z4",
        name: "zlah1"
    },
    {
        id: "Z5",
        name: "blah2"
    },
    {
        id: "Z6",
        name:  "clah3"
    }
]

c.sort_by! do |c|
 pc.index c[:id] 
end

This procedure returns:

=> [{:id=>"Z6", :name=>"clah3"}, {:id=>"Z5", :name=>"blah2"}, {:id=>"Z4", :name=>"zlah1"}]

It somehow reverses the array order. How does it do that? pc.index c[:id] just returns a number. What does this method do under the hood? The documentation is not very beginners friendly.

A:

Suppose we are given the following:
order = ['Z6', 'Z5', 'Z4']
array = [{id: 'Z4', name: 'zlah1'},
         {id: 'Z5', name: 'blah2'},
         {id: 'Z6', name: 'clah3'},
         {id: 'Z5', name: 'dlah4'}]

Notice that I added a 4th hash ({id: 'Z5', name: 'dlah4'}) to the array array given in the question. I did this so that two elements of array would the same value for the key :id ("Z5").
Now let's consider how Ruby might implement the following:
array.sort_by { |hash| order.index(hash[:id]) }
  #=> [{:id=>"Z6", :naCme=>"clah3"},
  #    {:id=>"Z5", :name=>"blah2"},
  #    {:id=>"Z5", :name=>"dlah4"},
  #    {:id=>"Z4", :name=>"zlah1"}] 

That could be done in four steps.
Step 1: Create a hash that maps the values of the sort criterion to the values of sort_by's receiver
sort_map = array.each_with_object(Hash.new { |h,k| h[k] = [] }) do |hash,h|
  h[order.index(hash[:id])] << hash
end
  #=> {2=>[{:id=>"Z4", :name=>"zlah1"}],
  #    1=>[{:id=>"Z5", :name=>"blah2"}, {:id=>"Z5", :name=>"dlah4"}],
  #    0=>[{:id=>"Z6", :name=>"clah3"}]}

h = Hash.new { |h,k| h[k] = [] } creates an empty hash with a default proc that operates as follows when evaluating:
h[k] << hash

If h has a key k this operation is performed as usual. If, however, h does not have a key k the proc is called, causing the operation h[k] = [] to be performed, after which h[k] << hash is executed as normal.
The values in this hash must be arrays, rather than individual elements of array, due the possibility that, as here, two elements of sort_by's receiver map to the same key. Note that this operation has nothing to do with the particular mapping of the elements of sort_by's receiver to the sort criterion.
Step 2: Sort the keys of sort_map
keys = sort_map.keys
  #=> [2, 1, 0] 
sorted_keys = keys.sort
  #=> [0, 1, 2]

Step 3: Map sorted_keys to the values of sort_map
sort_map_values = sorted_keys.map { |k| sort_map[k] }
  #=> [[{:id=>"Z6", :name=>"clah3"}],
  #    [{:id=>"Z5", :name=>"blah2"}, {:id=>"Z5", :name=>"dlah4"}],
  #    [{:id=>"Z4", :name=>"zlah1"}]]

Step 4: Flatten sort_map_values
sort_map_values.flatten
  #=> [{:id=>"Z6", :name=>"clah3"},
  #    {:id=>"Z5", :name=>"blah2"},
  #    {:id=>"Z5", :name=>"dlah4"},
  #    {:id=>"Z4", :name=>"zlah1"}] 

One of the advantages of using sort_by rather than sort (with a block) is that the sort criterion (here order.index(hash[:id])) is computed only once for each element of sort_by's receiver, whereas sort would recompute these values for each pairwise comparison in its block. The time savings can be considerable if this operation is computationally expensive.  

