36

I am trying to sort a Hash alphabetically by key, but I can't seem to find a way to do it without creating my own Sorting class. I found the code below to sort by value if it's an integer and I am trying to modify it but not having any luck.

temp["ninjas"]=36
temp["pirates"]=12
temp["cheese"]=222
temp.sort_by { |key, val| key }

My goal is to order the Hash by key then output the values. I will have to do this multiple times with different hash orders but the same values.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Rilcon42
  • 9,584
  • 18
  • 83
  • 167
  • 1
    The code appears to do what you requested. Would you edit the question to include the output you expect? – Don Cruickshank Dec 09 '14 at 17:26
  • I have made the assumption that you want your output to be another hash. It would be good to see that in the question if so (then the question and answer would match). – Neil Slater Dec 09 '14 at 17:27
  • 1
    What do you want as the output? Hashes aren't really sorted (they are, by insertion order, as of Ruby 1.9+). What specifically are you trying to do? – Dave Newton Dec 09 '14 at 17:27

5 Answers5

56

Assuming you want the output to be a hash which will iterate through keys in sorted order, then you are nearly there. Hash#sort_by returns an Array of Arrays, and the inner arrays are all two elements.

Ruby's Hash has a constructor that can consume this output.

Try this:

temp = Hash[ temp.sort_by { |key, val| key } ]

or more concisely

temp = temp.sort_by { |key| key }.to_h

If your hash has mixed key types, this will not work (Ruby will not automatically sort between Strings and Symbols for instance) and you will get an error message like comparison of Symbol with String failed (ArgumentError). If so, you could alter the above to

temp = Hash[ temp.sort_by { |key, val| key.to_s } ] 

to work around the issue. However be warned that the keys will still retain their original types which could cause problems with assumptions in later code. Also, most built-in classes support a .to_s method, so you may get unwanted results from that (such as unexpected sort order for numeric keys, or other unexpected types).

You could, in addition, convert the keys to Strings with something like this:

temp = Hash[ temp.map { |key, val| [key.to_s, val] }.sort ] 

. . . although this approach would lose information about the type of the original key making it impossible to refer back to the original data reliably.

Obromios
  • 15,408
  • 15
  • 72
  • 127
Neil Slater
  • 26,512
  • 6
  • 76
  • 94
  • I tried your code but I got: in 'sort_by': comparison of Symbol with String failed (ArgumentError) – Rilcon42 Dec 09 '14 at 17:33
  • Your hash has mixed key types and you will have this problem however you want to sort them. You could alter it to `temp = Hash[ temp.sort_by { |key, val| key.to_s } ]` to work around the issue, but be careful, that will change the Symbol keys into Strings . . . if you do really want to work through the data in sorted order, you will need to decide to one data type for your keys and stick with it – Neil Slater Dec 09 '14 at 17:35
  • would you mind updating your answer to make note of how to fix the problem for future users who may not read the comments? Thanks! – Rilcon42 Dec 09 '14 at 18:10
  • 1
    The last part isn't true. `sort_by` doesn't alter keys, it just uses the value from the block to sort. The argument passed to `Hash[]` constructor has the same keys as original hash. – Kazim Zaidi Nov 28 '17 at 05:21
  • @KazimZaidi: Thanks. That mistake has been in there for ages . . . I'll re-phrase it, there is still a concern about what it means to sort by stringified values and then preserve the original values. For instance if the keys were integers, the hash would not necessarily be sorted in a useful fashion. – Neil Slater Nov 28 '17 at 08:10
11
sorted_by_key = Hash[original_hash.sort]

will create a new Hash by inserting the key/values of original_hash alphabetically by key. Ruby 2.x hashes remember their insertion order, so this new hash will appear sorted by key if you enumerate it or output it.

If you insert more elements in a non-alphabetical order, this won't hold of course.

Also, this assumes the original hash keys are all sortable/comparable.

Trashpanda
  • 14,758
  • 3
  • 29
  • 18
6

Ruby's Hash remembers its insertion order now days, but earlier Rubies < v1.9 don't. But, don't bother sorting a hash as there is no advantage to doing so because basically a Hash is a random-access structure. That means the elements are all accessible at any time and it won't make a difference whether one is first or last, you can access it just the same.

That's unlike an Array which acts like a sequential/text file or a chain or a queue and you have to access it linearly by iterating over it, which, at that point, the order of the elements makes a big difference.

So, with a Hash, get the keys, sort them, and either iterate over the list of keys or use values_at to retrieve all the values at once. For instance:

hash = {
    'z' => 9,
    'a' => 1
}

sorted_keys = hash.keys.sort # => ["a", "z"]
sorted_keys.each do |k|
  puts hash[k]
end
# >> 1
# >> 9

hash.values_at(*sorted_keys) # => [1, 9]

Some languages won't even let you sort the hash, and accessing it via a sorted list of keys is the only way to extract the elements in an order, so it's probably a good idea to not get in the habit of relying on order of the key/value pairs, and instead rely on the keys.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • 3
    Your statement that sorting a hash "has no advantage" is only true in the lookup case. If you want to know *about* the hash. (e.g., creating an md5 of the contents of the hash), a consistent order is relevant and necessary. – MissingHandle Oct 29 '17 at 21:14
  • Since the question is specifically about doing lookups, your comment has little relevance. Yes, creating a MD5 of the entire object is occasionally helpful, but since the majority of the time people are retrieving values, sorting the hash won't help. – the Tin Man Nov 08 '17 at 19:47
  • 2
    Was only trying to refine the statement you made "don't bother sorting a hash as there is no advantage" for any noobs out there that could use their mind expanded as they learn :). Cheers! – MissingHandle Dec 03 '17 at 00:54
  • @theTinMan Where there are often so many ways to solve the same problem your insight is appreciated. Your comments did not cover my current use case though: how would you iterate over a hash sorted by values (I know, not OP's question, but still interested)? I used a hash to count occurrence of objects by using those objects as the key and increment the counter in the value. At the end I want to iterate the hash sorted by values. I ended up using `Hash#sort_by` followed by a normal `Hash#each`. Not sure if there is a more 'ruby' way of going about that use case.... What would you advise? – pjvleeuwen Jan 13 '20 at 21:15
  • @PaulvanLeeuwen `Hash#invert` is the starting point to ordering by values. Assign the result to a new variable. Tack on `keys` and `sort` and you should be real close. https://stackoverflow.com/q/10989259/128421 – the Tin Man Jan 14 '20 at 00:15
  • @PaulvanLeeuwen also see https://stackoverflow.com/q/16103164/128421 – the Tin Man Jan 14 '20 at 00:28
3

In addition to Neil Slater's answer, which uses the Hash#sort_by method (which is nice and concise when outputting comparable values in the block)...

irb(main):001:0> h = { a: 0, b: 5, c: 3, d: 2, e: 3, f:1 }
=> {:a=>0, :b=>5, :c=>3, :d=>2, :e=>3, :f=>1}
irb(main):002:0> h.sort_by { |pair| pair[1] }.to_h
=> {:a=>0, :f=>1, :d=>2, :c=>3, :e=>3, :b=>5}

...or the reverse variant...

irb(main):003:0> h.sort_by { |pair| pair[1] }.reverse.to_h
=> {:b=>5, :e=>3, :c=>3, :d=>2, :f=>1, :a=>0}

...there is also the option to use the Array#sort method which allows you to define your own comparison rules (e.g. this sorts by value ascending, but then by key descending on equal values):

irb(main):004:0> h.to_a.sort { |one, other| (one[1] == other[1]) ? other[0] <=> one[0] : one[1] <=> other[1] }.to_h
=> {:a=>0, :f=>1, :d=>2, :e=>3, :c=>3, :b=>5}

This last option is a bit less concise, but can be more flexible (e.g. custom logic to deal with a mixture of types).

pjvleeuwen
  • 4,215
  • 1
  • 18
  • 31
0

You can create a new empty hash to hold the sorted hash data. Iterate through the returned array and load the data into the new hash to hold the sorted hash data.

temp = {}
temp["ninjas"]=36
temp["pirates"]=12
temp["cheese"]=222 
temp = temp.sort_by { |key, val| key }

temp_sorted = {}
temp.each { |sub_arr| temp_sorted[sub_arr[0]] = sub_arr[1] } 
temp = temp_sorted

temp now equals {"cheese"=>222, "ninjas"=>36, "pirates"=>12}

Jake
  • 1