SKools
SKools

Reputation: 1

How can I transpose array of hashes in ruby

I have a following array of hashes as the input :-

input =[
{"ID"=>"100", "Key"=>"Field A", "Value"=>"123"}, 
{"ID"=>"100", "Key"=>"Field B", "Value"=>"333"}, 
{"ID"=>"100", "Key"=>"Field C", "Value"=>"555"}, 
{"ID"=>"200", "Key"=>"Field A", "Value"=>"789"}, 
{"ID"=>"200", "Key"=>"Field B", "Value"=>"999"},
{"ID"=>"200", "Key"=>"Field D", "Value"=>"444"}
]

I would like to transform this array of hash as below

output =[
{"ID"=>"100", "Field A"=>"123", "Field B"=>"333", "Field C" => "555", "Field D" => ""}, 
{"ID"=>"200", "Field A"=>"789", "Field B"=>"999", "Field C" => "", "Field D" => "444"}
]

I can fetch unique ID and keys as below

irb(main):099:0> unique_id = input.map { |p| p["ID"] }.uniq
=> ["100", "200"]
irb(main):100:0> unique_keys = input.map { |p| p["Key"] }.uniq
=> ["Field A", "Field B", "Field C", "Field D"]

However, I am not able to proceed beyond this to create unique array of hashes for each ID containing keys/value pairs as defined on the input hash.

Upvotes: 0

Views: 464

Answers (5)

the Tin Man
the Tin Man

Reputation: 160571

The output structure isn't what I'd ever want to work with, and it looks like the input structure is influencing the desired output. That results in an XY problem.

Hashes are extremely efficient, especially when you have something that acts like an index field in a database. Iterating over an array to find a value is extremely inefficient in comparison to a hash, so I'd recommend taking a second look at those two structures.

Converting the input to a true hash isn't hard:

input = [
  {"ID"=>"100", "Key"=>"Field A", "Value"=>"123"},  
  {"ID"=>"100", "Key"=>"Field B", "Value"=>"333"},  
  {"ID"=>"100", "Key"=>"Field C", "Value"=>"555"},  
  {"ID"=>"200", "Key"=>"Field A", "Value"=>"789"},  
  {"ID"=>"200", "Key"=>"Field B", "Value"=>"999"},  
  {"ID"=>"200", "Key"=>"Field D", "Value"=>"444"}   
]                                                   

output = Hash.new { |h, k| h[k] = {} }  # => {}
input.each { |e|      
  id = e['ID']        
  key = e['Key']      
  value = e['Value']  

  output[id][key] = value 
}                         

Which results in:

output
# => {"100"=>{"Field A"=>"123", "Field B"=>"333", "Field C"=>"555"},
#     "200"=>{"Field A"=>"789", "Field B"=>"999", "Field D"=>"444"}}

The benefit of this is pretty obvious, if you want the data for "200" it's easy to grab:

output['200'] # => {"Field A"=>"789", "Field B"=>"999", "Field D"=>"444"}
output['200']['Field B'] # => "999"

Upvotes: 0

3limin4t0r
3limin4t0r

Reputation: 21130

Something like this might do the job:

keys = input.map { |hash| hash['Key'] }.uniq
result = Hash.new { |result, id| result[id] = {} }
input.each { |hash| result[hash['ID']].merge!(hash['Key'] => hash['Value']) }
result.default = nil # optional: remove the default value

result.each do |id, hash| 
  (keys - hash.keys).each { |key| hash[key] = '' }
  hash['ID'] = id
end    

result.values
#=> [{"Field A"=>"123", "Field B"=>"333", "Field C"=>"555", "Field D"=>"", "ID"=>"100"},
#    {"Field A"=>"789", "Field B"=>"999", "Field D"=>"444", "Field C"=>"", "ID"=>"200"}]

If you're certain values are never falsy you can replace:

(keys - hash.keys).each { |key| hash[key] = '' }
# with
keys.each { |key| hash[key] ||= '' }

I first create a hash result to save the resulting hashes, I set the value to defaults to a new hash. Then I get the correct hash based upon ID and merge the key-value pairs into the hash. Lastly I add the missing keys to the hashes and set their values to an empty string and add the ID under which the hash is saved to the hash.

note: If your input array contains duplicate key-value pairs, the last one will be used. For example, say both {"ID"=>"100", "Key"=>"Field A", "Value"=>"123"} and {"ID"=>"100", "Key"=>"Field A", "Value"=>"456"} are present. Then "Field A" => "456" will be set, since it's the latter of the two.

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110725

My answer has three steps.

Step 1: Obtain the unique values of "ID" and the unique keys of the form "Field X"

ids, keys = input.map { |h| h.values_at("ID", "Key") }.transpose.map(&:uniq)
  #=> [["100", "200"], ["Field A", "Field B", "Field C", "Field D"]] 

See Hash#values_at. The calculations are as follows:

a = input.map { |h| h.values_at("ID", "Key") }
  #=> [["100", "Field A"], ["100", "Field B"], ["100", "Field C"],
  #    ["200", "Field A"], ["200", "Field B"], ["200", "Field D"]] 
b = a.transpose
  #=> [["100", "100", "100", "200", "200", "200"],
  #    ["Field A", "Field B", "Field C", "Field A", "Field B", "Field D"]] 
ids, keys = b.map(&:uniq)
  #=> [["100", "200"], ["Field A", "Field B", "Field C", "Field D"]] 
ids
  #=> ["100", "200"] 
keys
  #=> ["Field A", "Field B", "Field C", "Field D"] 

Step 2: Construct a hash whose keys are the unique values of "ID" and whose values are hashes to be completed and extracted in Step 3

h = ids.each_with_object({}) { |id,h|
  h[id] = keys.each_with_object("ID"=>id) { |key,g| g[key] = "" } }
  #=> {"100"=>{"ID"=>"100", "Field A"=>"", "Field B"=>"", "Field C"=>"",
  #            "Field D"=>""},
  #    "200"=>{"ID"=>"200", "Field A"=>"", "Field B"=>"", "Field C"=>"",
  #            "Field D"=>""}}

Step 3: Loop through input to complete the values of the hash constructed in Step 2, then, as a final step, extract the values from that hash

input.each_with_object(h) { |g,h| h[g["ID"]].update(g["Key"]=>g["Value"]) }.values
  #=> [{"ID"=>"100", "Field A"=>"123", "Field B"=>"333", "Field C"=>"555",
  #     "Field D"=>""},
  #    {"ID"=>"200", "Field A"=>"789", "Field B"=>"999", "Field C"=>"",
  #     "Field D"=>"444"}]

See Hash#update (aka merge!) and Hash#values. The two calculations are as follows:

h = input.each_with_object(h) { |g,h| h[g["ID"]].update(g["Key"]=>g["Value"]) }
  #=> {"100"=>{"ID"=>"100", "Field A"=>"123", "Field B"=>"333","Field C"=>"555",
  #            "Field D"=>""},
  #    "200"=>{"ID"=>"200", "Field A"=>"789", "Field B"=>"999","Field C"=>"",
  #            "Field D"=>"444"}} 
h.values
  #=> <as above>     

Upvotes: 1

SKools
SKools

Reputation: 1

keys = input.map { |hash| hash['Key'] }.uniq

output = input.group_by  { |x| x['ID'] }.map { |k,v| ([['ID', k]] + v.map {|z| z.values_at('Key','Value') }).to_h }

output.map! { |x| {'ID' => x['ID']}.merge fields.map {|z| [z, x[z].to_s]}.to_h }

The following gives me the output as shown below

[
 {"ID"=>"100", "Field A"=>"123", "Field B"=>"333", "Field C"=>"555", "Field D"=>""}, 
 {"ID"=>"200", "Field A"=>"789", "Field B"=>"999", "Field C"=>"", "Field D"=>"444"}
]

thanks you everyone for your input

Upvotes: -2

ray
ray

Reputation: 5552

Try Following,

fields = input.map {|x| x['Key'] }.uniq

output = input.group_by  { |x| x['ID'] }
     .map { |k,v| ([['ID', k]] + v.map {|z| z.values_at('Key','Value') }).to_h }

output.map! { |x| {'ID' => x['ID']}.merge fields.to_h {|z| [z, x[z].to_s]} }

Output will be,

[
  {"ID"=>"100", "Field A"=>"123", "Field B"=>"333", "Field C"=>"555", "Field D"=>""}, 
  {"ID"=>"200", "Field A"=>"789", "Field B"=>"999", "Field C"=>"", "Field D"=>"444"}
]

Upvotes: 3

Related Questions