Stephen C
Stephen C

Reputation: 863

Sort hash by key which is a string

Assuming I get back a string:

"27,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,12,17,17,41,17,17,17,17,17,17,17,17,17,17,17,17,17,26,26,26,26,26,26,26,26,26,29,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,40,48,28,28,28,28,28,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,34,34,34,34,34,34,36,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,43,43,43,43,43,44,44,44,44,48,49,29,41,6,30,11,29,29,36,29,29,36,29,43,1,29,29,29,1,41"

I turn that into an array by calling

str.split(',')

Then turning it into a hash by calling

arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }

I would get back a hash that looks like

{"1"=>2, "6"=>1, "39"=>23, "36"=>23, "34"=>39, "32"=>31, "30"=>18, "3"=>8, "2"=>10, "28"=>36, "29"=>21, "26"=>41, "27"=>48, "49"=>1, "44"=>4, "43"=>14, "42"=>34, "48"=>2, "40"=>9, "41"=>10, "11"=>1, "17"=>15, "12"=>1}

However, I'd like to sort that hash by key.

I've tried the solutions listed here.

I believe my problem is related to the fact they keys are strings.

The closest I got was using

Hash[h.sort_by{|k,v| k.to_i}]

Upvotes: 1

Views: 1091

Answers (5)

Cary Swoveland
Cary Swoveland

Reputation: 110755

Suppose you started with

str = "27,2,2,2,41,26,26,26,48,48,41,6,11,1,41"

and created the following hash

h = str.split(',').inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
  #=> {"27"=>1, "2"=>3, "41"=>3, "26"=>3, "48"=>2, "6"=>1, "11"=>1, "1"=>1}

I removed compact because the array str.split(',') contains only (possibly empty) strings, no nils.

Before continuing, you may want to change this last step to

h = str.split(/\s*,\s*/).each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
  #=> {"27"=>1, "2"=>3, "41"=>3, "26"=>3, "48"=>2, "6"=>1, "11"=>1, "1"=>1}

Splitting on the regex allows for the possibility of one or more spaces before or after each comma, and Enumerable#each_with_object avoids the need for that pesky ; h. (Notice the block variables are reversed.)

Then

h.sort_by { |k,_| k.to_i }.to_h
  #=> {"1"=>1, "2"=>3, "6"=>1, "11"=>1, "26"=>3, "27"=>1, "41"=>3, "48"=>2}

creates a new hash that contains h's key-value pairs sorted by the integer representations of the keys. See Hash#sort_by.

Notice we've created two hashes. Here's a way to do that by modifying h in place.

h.keys.sort_by(&:to_i).each { |k| h[k] = h.delete(k) }
  #=> ["1", "2", "6", "11", "26", "27", "41", "48"] (each always returns the receiver)
h #=> {"1"=>1, "2"=>3, "6"=>1, "11"=>1, "26"=>3, "27"=>1, "41"=>3, "48"=>2}

Lastly, another alternative is to sort str.split(',') before creating the hash.

str.split(',').sort_by(&:to_i).each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
  #=> {"1"=>1, "2"=>3, "6"=>1, "11"=>1, "26"=>3, "27"=>1, "41"=>3, "48"=>2}

Upvotes: 3

hirolau
hirolau

Reputation: 13921

A Ruby hash will keep the order of keys added. If the array is small enough to sort I would just change

str.split(',').

to

str.split(',').sort_by(&:to_i)

in order to get the values, and therefore also you hash sorted...

Upvotes: 0

Eric Duminil
Eric Duminil

Reputation: 54303

Notes

compact

String#split cannot return a nil element. compact won't be useful, here. split might return an empty string, though :

p "1,,2,3".split(',')
# ["1", "", "2", "3"]
p "1,,2,3".split(',').compact
# ["1", "", "2", "3"]
p "1,,2,3".split(',').reject(&:empty?)
# ["1", "2", "3"]

inject

If you have to use two statements inside inject block, each_with_object might be a better idea :

arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }

can be rewritten :

arr.compact.each_with_object(Hash.new(0)) { |e, h| h[e] += 1 }

Hash or Array?

If you need to sort results, an Array of pairs might be more suitable than a Hash.

String or Integer?

If you accept to have an integer as key, it might make your code easier to write.

Refactoring

Here's a possibility to rewrite your code :

str.split(',')
   .reject(&:empty?)
   .map(&:to_i)
   .group_by(&:itself)
   .map { |k, v| [k, v.size] }
   .sort

It outputs :

[[1, 2], [2, 10], [3, 8], [6, 1], [11, 1], [12, 1], [17, 15], [26, 41], [27, 48], [28, 36], [29, 21], [30, 18], [32, 31], [34, 39], [36, 23], [39, 23], [40, 9], [41, 10], [42, 34], [43, 14], [44, 4], [48, 2], [49, 1]]

If you really want a Hash, you can add .to_h :

{1=>2, 2=>10, 3=>8, 6=>1, 11=>1, 12=>1, 17=>15, 26=>41, 27=>48, 28=>36, 29=>21, 30=>18, 32=>31, 34=>39, 36=>23, 39=>23, 40=>9, 41=>10, 42=>34, 43=>14, 44=>4, 48=>2, 49=>1}

Upvotes: 2

spickermann
spickermann

Reputation: 107142

Hashes shouldn't be treated as a sorted data structure. They have other advantages and use case as to return their values sequentially. As Mladen Jablanović already pointed out a array of tuples might be the better data structure when you need a sorted key/value pair.

But in current versions of Ruby there actually exists a certain order in which key/value pairs are returned when you call for example each on a hash and that is the order of insertion. Using this behavior you can just build a new hash and insert all key/value pairs into that new hash in the order you want them to be. But keep in mind that the order will break when you add more entries later on.

string = "27,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,12,17,17,41,17,17,17,17,17,17,17,17,17,17,17,17,17,26,26,26,26,26,26,26,26,26,29,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,40,48,28,28,28,28,28,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,34,34,34,34,34,34,36,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,43,43,43,43,43,44,44,44,44,48,49,29,41,6,30,11,29,29,36,29,29,36,29,43,1,29,29,29,1,41"

sorted_number_count_tupels = string.split(',').
                                    group_by(&:itself).
                                    map { |k, v| [k, v.size] }.
                                    sort_by { |(k, v)| k.to_i }
#=> [["1",2],["2",10],["3",8],["6",1],["11",1],["12",1],["17",15],["26",41],["27",48],["28",36],["29",21],["30",18],["32",31],["34",39],["36",23],["39",23],["40",9],["41",10],["42",34],["43",14],["44",4],["48",2],["49",1]]

sorted_number_count_hash = sorted_number_count_tupels.to_h
#=> { "1" => 2, "2" => 10, "3" => 8, "6" => 1, "11" => 1, "12" => 1, "17" => 15, "26" => 41, "27" => 48, "28" => 36, "29" => 21, "30" => 18, "32" => 31, "34" => 39, "36" => 23, "39" => 23, "40" => 9, "41" => 10, "42" => 34, "43" => 14, "44" => 4, "48" => 2, "49" => 1}

Upvotes: 5

LBarry
LBarry

Reputation: 115

You can assign the arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } to a variable and sort it by key: num = arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } num.keys.sort

That would sort the hash by key.

Upvotes: 0

Related Questions