5

Say I have a Hash that looks like this in Ruby:

{ :ie0 => "Hi",    :ex0 => "Hey",       :eg0 => "Howdy", 
  :ie1 => "Hello", :ex1 => "Greetings", :eg1 => "Good day"}

What is a good way to turn that into something like:

{ "0" => 
    { 
        "ie" => "Hi", "ex" => "Hey", "eg" => "Howdy"
    },
  "1" => 
    {
        "ie" => "Hello", "ex" => "Greetings", "eg" => "Good day"
    }
}
tvalent2
  • 4,959
  • 10
  • 45
  • 87

5 Answers5

3

It's not pretty, but this works:

input = { :ie0 => "Hi",    :ex0 => "Hey",       :eg0 => "Howdy",
          :ie1 => "Hello", :ex1 => "Greetings", :eg1 => "Good day"}

output = input.inject({}) do |result, item|
  item[0] =~ /(?<key>[^\d]+)(?<index>\d+)/
  key, index = $1, $2
  value = item[1]

  result[index] ||= {}
  result[index].merge! { key => value }
  result
end

puts output
Bryan Ash
  • 4,385
  • 3
  • 41
  • 57
3

You asked for a good way to do it, so the answer is: a way that you or a co-worker can understand and maintain six months from now.

First, you want a Hash with autovivification because you're creating a nested hash structure. This is a very useful coding pattern which will simplify your application code:

# Copied & pasted from the question
old_hash = { :ie0 => "Hi", :ex0 => "Hey", :eg0 => "Howdy", :ie1 => "Hello", :ex1 => "Greetings", :eg1 => "Good day"}

# Auto-vivify
new_hash = Hash.new { |h, k| h[k] = { } }

Then, you can loop through your existing keys in this simple style, breaking out the parts of each key, and using them to save the value in the new hash:

old_hash.each_pair do |key, value|
  key =~ /^(..)(.)$/               # Make a regex group for each string to parse
  new_hash[$2][$1] = value         # The regex groups become the new hash keys
end

puts new_hash

I get this output:

{"0"=>{"ie"=>"Hi", "ex"=>"Hey", "eg"=>"Howdy"}, "1"=>{"ie"=>"Hello", "ex"=>"Greetings", "eg"=>"Good day"}}
Community
  • 1
  • 1
Dogweather
  • 15,512
  • 17
  • 62
  • 81
  • Yeah I got the same thing. Didn't know about auto-vivification though! – tvalent2 Oct 05 '13 at 05:11
  • Works great for me, Ruby 2.0.0. I'll edit to add the full code. – Dogweather Oct 05 '13 at 05:14
  • Ah, just didn't `puts new_hash`. Awesome, thank you! – tvalent2 Oct 05 '13 at 05:18
  • Interesting approach. I can see how using a regex in this way has lots of potential applications in operations involving iteration. – Cary Swoveland Oct 05 '13 at 06:23
  • Yes, @CarySwoveland, I use this all the time when I know in advance that the string is a "match". Regex Groups are a nice way to parse out information, making it easy to come back later and understand what the code is doing. – Dogweather Oct 05 '13 at 23:23
  • I would take issue with one part of your answer. You said, "...that you or a co-worker can understand and maintain six months from now." I thought the main objective was to impress your co-worker with how smart you are. – Cary Swoveland Oct 06 '13 at 00:36
  • A small suggestion: change `key =~ /^(..)(.)$/` to something like `key =~ /([^\d]+)(\d+)/` to permit the integer part of the key to have more than a single digit. – Cary Swoveland Oct 06 '13 at 00:53
  • @CarySwoveland I don't think that regex will correctly match the whole key. Also, multiple-digits aren't in the requirements, so I'd keep it simple and use this regex as a format check as well. I.e., if the integer portion *does* have multiple digits, then that's an error in the data. – Dogweather Oct 07 '13 at 00:42
2
require 'awesome_print'

hsh = {
           :ie0 => "Hi",
           :ex0 => "Hey",
           :eg0 => "Howdy",
           :ie1 => "Hello",
           :ex1 => "Greetings",
           :eg1 => "Good day"
      }

new_hsh = hsh.each_with_object(Hash.new { |h, k| h[k] = { } }) do |(k,v),h|
       h[k[-1]].merge!({k[0..-2] => v})
end
ap new_hsh

output (formatted with awesome_print)

{
    "0" => {
        "ie" => "Hi",
        "ex" => "Hey",
        "eg" => "Howdy"
    },
    "1" => {
        "ie" => "Hello",
        "ex" => "Greetings",
        "eg" => "Good day"
    }
}
Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317
1

Edit: with my apologies to the upvoter, I now rather dislke this answer (but not so much that I'll downvote it myself). I'll let it remain here for its historical significance (and for the 10 points, of course). Among its many shortcomings, it only permits single-digit keys in the result. I submitted another solution I like better.

h = {ie0: "Hi", ex0: "Hey", eg0: "Howdy", ie1: "Hello", ex1: "Greetings", eg1: "Good day"}

n = h.keys.map {|k| k.to_s[0..-2]}.uniq.size # => 3

Hash[*h.to_a.each_slice(n).to_a.map {|s| [s.first.first.to_s[-1], Hash[*s.map {|v| [v.first.to_s[0..-2], v.last]}.flatten]]}.flatten]
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

Here's an approach using @Mason's suggestion (in a comment on the question) to use group_by.

h = {ie0: "Hi", ex0: "Hey", eg0: "Howdy", ie999: "Hello", ex999: "Greetings", eg999: "Good day"}

Edit: new and improved(?):

p Hash[h.group_by {|e| e[0][/\d+/]}.map {|k,v| [k, Hash[v.map {|a| [a.first.to_s[/[^\d]+/], a.last]}]]}]

Formerly I had:

hh = h.group_by { |e| e[0][/\d+/] }
hh.each_key {|k| hh[k] = Hash[*[hh[k].map {|v| [v.first.to_s[/[^\d]+/], v.last]}].flatten]}

Suggestions are welcome. I've found this an interesting question and have enjoyed reading the other answers, which I found varied and innovative.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100