emico7
emico7

Reputation: 135

Sort array of arrays that is a value of a hash

I saw several posts on sorting an array that is a value of a hash, but I'm trying to sort an array of arrays that is a value of a hash. I have a hash that looks like this:

h = { 
      "pets"=>[["dog", 1], ["cat", 2]],
      "fruits"=>[["orange", 1], ["apple", 2]]
     }

I would like to sort array of arrays according to the first elements (string) of the arrays inside. So I want the result to be

{ 
  "pets"=>[["cat", 2], ["dog", 1]],
  "fruits"=>[["apple", 2], ["orange", 1]]
}

This is what I have right now:

h.map do |key, value|
  value.sort_by! { |x, y| x[0] <=> y[0] }
end

But this just returns the original array. What do I need to change? Thanks!

Upvotes: 1

Views: 379

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110675

h.merge(h) { |*, n| n.sort }
  #=> {"pets"=>[["cat", 2], ["dog", 1]], "fruits"=>[["apple", 2], ["orange", 1]]}

This does not modify h.

This uses the form of Hash#merge that employs a block to determine the values of keys that are present in both hashes being merged, which here is all keys. The block has three block variables, the common key k, the value of k for the "old hash, o and the value of k for the "new" hash, n. (Here o and n are of course equal.) I'm only using the last of these variables, so I've expressed the block variables as |*, n|, rather than |k, o, n|. See the doc for details.

Another way that does not mutate h:

h.each_key.zip(h.each_value.map(&:sort)).to_h
  #=> {"pets"=>[["cat", 2], ["dog", 1]], "fruits"=>[["apple", 2], ["orange", 1]]}

This could instead be written

h.keys.zip(h.values.map(&:sort)).to_h

Upvotes: 3

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Immutable version, having no side effects:

h.map { |k, v| [k, v.sort_by(&:first)] }.to_h

Upvotes: 3

Rockster160
Rockster160

Reputation: 1648

The one issue I see is that you are attempting to map your hash. That will always return an array.

In this case, you’ll get back something like this: [[["cat", 2], ["dog", 1]], [["apple", 2], ["orange", 1]]]

If you want to modify your hash in place, you can do:

h.each { |key, value| h[key] = value.sort_by { ... } }

Then h will be the new hash, sorted as you expect. Or you can set it to another variable:

new_hash = h.each { |key, value| value.sort_by! { ... } }

Next, it looks like you’ve confused sort and sort_by. (Easy to do) When you use sort, the two variables you pass in your block are assigned to the values being compared. (In your case, the arrays ["dog", 1] is one argument, and the other would be another array to compare it to, like ["cat", 2]) However, sort_by expects you to do the computing to determine where it’s place is in the new order. Normally sort_by only accepts a single argument, but because you’re working with arrays (and because of the way Ruby handles tuples) when you tell sort_by to accept 2 arguments (x and y) those are set to the first and second values of your array. (x == "dog", y == 1)

So you’ve got 2 options here: Keep your block the same and switch your enumerable method to sort, or switch to sort_by and change your block:

sort:

h.each { |key, value| h[key] = value.sort! { |x, y| x[0] <=> y[0] } }

sort_by:

h.each { |key, value| h[key] = value.sort_by! { |x, y| y } } # In this case, `y` is the second value in your arrays, which is the integer

Upvotes: 1

Related Questions