Convert Nested Array into Nested Hash in Ruby - ruby

Without knowing the dimension of array, how do I convert an array to a nested hash?
For example:
[["Message", "hello"]]
to:
{{:message => "Hello"}}
Or:
[["Memory", [["Internal Memory", "32 GB"], ["Card Type", "MicroSD"]]]]
to:
{{:memory => {:internal_memroy => "32 GB", :card_type => "MicroSD"}}}
or:
[["Memory", [["Internal Memory", "32 GB"], ["Card Type", "MicroSD"]]], ["Size", [["Width", "12cm"], ["height", "20cm"]]]]
to:
{ {:memory => {:internal_memroy => "32 GB", :card_type => "MicroSD"}, {:size => {:width => "12cm", :height => "20cm" } } }

Considering your format of nested arrays of pairs, that following function transforms it into the hash you'd like
def nested_arrays_of_pairs_to_hash(array)
result = {}
array.each do |elem|
second = if elem.last.is_a?(Array)
nested_arrays_to_hash(elem.last)
else
elem.last
end
result.merge!({elem.first.to_sym => second})
end
result
end
A shorter version
def nested_arrays_to_hash(array)
return array unless array.is_a? Array
array.inject({}) do |result, (key, value)|
result.merge!(key.to_sym => nested_arrays_to_hash(value))
end
end

> [:Message => "hello"]
=> [{:Message=>"hello"}]
Thus:
> [:Message => "hello"][0]
=> {:Message=>"hello"}

Related

Convert array into hash and add a counter value to the new hash

I have the following array of hashes:
[
{"BREAD" => {:price => 1.50, :discount => true }},
{"BREAD" => {:price => 1.50, :discount => true }},
{"MARMITE" => {:price => 1.60, :discount => false}}
]
And I would like to translate this array into a hash that includes the counts for each item:
Output:
{
"BREAD" => {:price => 1.50, :discount => true, :count => 2},
"MARMITE" => {:price => 1.60, :discount => false, :count => 1}
}
I have tried two approaches to translate the array into a hash.
new_cart = cart.inject(:merge)
hash = Hash[cart.collect { |item| [item, ""] } ]
Both work but then I am stumped at how to capture and pass the count value.
Expected output
{
"BREAD" => {:price => 1.50, :discount => true, :count => 2},
"MARMITE" => {:price => 1.60, :discount => false, :count => 1}
}
We are given the array:
arr = [
{"BREAD" => {:price => 1.50, :discount => true }},
{"BREAD" => {:price => 1.50, :discount => true }},
{"MARMITE" => {:price => 1.60, :discount => false}}
]
and make the assumption that each hash has a single key and if two hashes have the same (single) key, the value of that key is the same in both hashes.
The first step is create an empty hash to which will add key-value pairs:
h = {}
Now we loop through arr to build the hash h. I've added a puts statement to display intermediate values in the calculation.
arr.each do |g|
k, v = g.first
puts "k=#{k}, v=#{v}"
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count => 1 })
end
end
displays:
k=BREAD, v={:price=>1.5, :discount=>true}
k=BREAD, v={:price=>1.5, :discount=>true}
k=MARMITE, v={:price=>1.6, :discount=>false}
and returns:
#=> [{"BREAD" =>{:price=>1.5, :discount=>true}},
# {"BREAD" =>{:price=>1.5, :discount=>true}},
# {"MARMITE"=>{:price=>1.6, :discount=>false}}]
each always returns its receiver (here arr), which is not what we want.
h #=> {"BREAD"=>{:price=>1.5, :discount=>true, :count=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :count=>1}}
is the result we need. See Hash#key? (aka, has_key?), Hash#[], Hash#[]= and Hash#merge.
Now let's wrap this in a method.
def hashify(arr)
h = {}
arr.each do |g|
k, v = g.first
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count=>1 })
end
end
h
end
hashify(arr)
#=> {"BREAD"=>{:price=>1.5, :discount=>true, :count=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :count=>1}}
Rubyists would often use the method Enumerable#each_with_object to simplify.
def hashify(arr)
arr.each_with_object({}) do |g,h|
k, v = g.first
if h.key?(k)
h[k][:count] += 1
else
h[k] = v.merge({ :count => 1 })
end
end
end
Compare the two methods to identify their differences. See Enumerable#each_with_object.
When, as here, the keys are symbols, Ruby allows you to use the shorthand { count: 1 } for { :count=>1 }. Moreover, she permits you to write :count = 1 or count: 1 without the braces when the hash is an argument. For example,
{}.merge('cat'=>'meow', dog:'woof', :pig=>'oink')
#=> {"cat"=>"meow", :dog=>"woof", :pig=>"oink"}
It's probably more common to see the form count: 1 when keys are symbols and for the braces to be omitted when a hash is an argument.
Here's a further refinement you might see. First create
h = arr.group_by { |h| h.keys.first }
#=> {"BREAD" =>[{"BREAD"=>{:price=>1.5, :discount=>true}},
# {"BREAD"=>{:price=>1.5, :discount=>true}}],
# "MARMITE"=>[{"MARMITE"=>{:price=>1.6, :discount=>false}}]}
See Enumerable#group_by. Now convert the values (arrays) to their sizes:
counts = h.transform_values { |arr| arr.size }
#=> {"BREAD"=>2, "MARMITE"=>1}
which can be written in abbreviated form:
counts = h.transform_values(&:size)
#=> {"BREAD"=>2, "MARMITE"=>1}
See Hash#transform_values. We can now write:
uniq_arr = arr.uniq
#=> [{"BREAD"=>{:price=>1.5, :discount=>true}},
#= {"MARMITE"=>{:price=>1.6, :discount=>false}}]
uniq_arr.each_with_object({}) do |g,h|
puts "g=#{g}"
k,v = g.first
puts " k=#{k}, v=#{v}"
h[k] = v.merge(counts: counts[k])
puts " h=#{h}"
end
which displays:
g={"BREAD"=>{:price=>1.5, :discount=>true}}
k=BREAD, v={:price=>1.5, :discount=>true}
h={"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2}}
g={"MARMITE"=>{:price=>1.6, :discount=>false}}
k=MARMITE, v={:price=>1.6, :discount=>false}
h={"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2},
"MARMITE"=>{:price=>1.6, :discount=>false, :counts=>1}}
and returns:
#=> {"BREAD"=>{:price=>1.5, :discount=>true, :counts=>2},
# "MARMITE"=>{:price=>1.6, :discount=>false, :counts=>1}}
See Array#uniq.
This did the trick:
arr = [
{ bread: { price: 1.50, discount: true } },
{ bread: { price: 1.50, discount: true } },
{ marmite: { price: 1.60, discount: false } }
]
Get the count for each occurrence of hash, add as key value pair and store:
h = arr.uniq.each { |x| x[x.first.first][:count] = arr.count(x) }
Then convert hashes into arrays, flatten to a single array then construct a hash:
Hash[*h.collect(&:to_a).flatten]
#=> {:bread=>{:price=>1.50, :discount=>true, :count=>2}, :marmite=>{:price=>1.60, :discount=>false, :count=>1}}
Combined a couple of nice ideas from here:
https://raycodingdotnet.wordpress.com/2013/08/05/array-of-hashes-into-single-hash-in-ruby/
and here:
http://carol-nichols.com/2015/08/07/ruby-occurrence-couting/

How to generate direct access keys to nested hash which contains hash and arrays as values?

I want to compare two XML files where one is input and the other is output. I am converting both into a hash.
My idea is to get all the keys from the input XML converted to hash, and search each key in both the input and output hashes for their respective key/value pairs.
I have a hash:
{
"requisition_header" => {
"requested_by" => {"login" => "coupasupport"},
"department" => {"name" => "Marketing"},
"ship_to_address" => {"name" => "Address_1431693296"},
"justification" => nil,
"attachments" => [],
"requisition_lines" => [
{
"description" => "Cleaning Services for Building A",
"line_num" => 1,
"need_by_date" => 2010-09-23 07:00:00 UTC,
"source_part_num" => nil,
"supp_aux_part_num" => nil,
"unit_price" => #<BigDecimal:a60520c,'0.3E4',9(18)>,
"supplier" => {"name" => "amazon.com"},
"account" => {
"code" => "SF-Marketing-Indirect",
"account_type" => {"name" => "Ace Corporate"}
},
"currency" => {"code" => "USD"},
"payment_term" => {"code" => "Net 30"},
"shipping_term" => {"code" => "Standard"},
"commodity" => {"name" => "Marketing-Services"}
}
]
}
}
It is nested and all the values are not directly accessible.
I want a way to generate direct access to each value in the hash.
For example:
requisition_header.requested_by.login
will access "coupasupport".
requisition_header.department.name
will access "Marketing".
requisition_header.requisition_lines[0].description
will access "Cleaning Services for Building A".
requisition_header.requisition_lines[0].line_num
will access "1".
requisition_header.requisition_lines[0].need_by_date
will access "2010-09-23 07:00:00 UTC".
Each key built can be used to search for the value directly inside the hash.
That could be done with the following method, that translates the nested hash into nested OpenStructs:
require 'ostruct'
def deep_structify(hash)
result = {}
hash.each do |key, value|
result[key] = value.is_a?(Hash) ? deep_structify(value) : value
end if hash
OpenStruct.new(result)
end
hash = {"requisition_header"=>{"requested_by"=>{"login"=>"coupasupport"}, "department"=>{"name"=>"Marketing"}, "ship_to_address"=>{"name"=>"Address_1431693296"}, "justification"=>nil, "attachments"=>[], "requisition_lines"=>[{"description"=>"Cleaning Services for Building A", "line_num"=>1, "need_by_date"=>2010-09-23 07:00:00 UTC, "source_part_num"=>nil, "supp_aux_part_num"=>nil, "unit_price"=>#<BigDecimal:a60520c,'0.3E4',9(18)>, "supplier"=>{"name"=>"amazon.com"}, "account"=>{"code"=>"SF-Marketing-Indirect", "account_type"=>{"name"=>"Ace Corporate"}}, "currency"=>{"code"=>"USD"}, "payment_term"=>{"code"=>"Net 30"}, "shipping_term"=>{"code"=>"Standard"}, "commodity"=>{"name"=>"Marketing-Services"}}]}}
struct = deep_structify(hash)
struct.requisition_header.department.name
#=> "Marketing"
You can do it by overriding OpenStruct#new as well,
require 'ostruct'
class DeepStruct < OpenStruct
def initialize(hash=nil)
#table = {}
#hash_table = {}
if hash
hash.each do |k,v|
#table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
#hash_table[k.to_sym] = v
new_ostruct_member(k)
end
end
end
def to_h
#hash_table
end
end
Now you can do:
require 'deep_struct'
hash = {"requisition_header"=>{"requested_by"=>{"login"=>"coupasupport"}, "department"=>{"name"=>"Marketing"}, "ship_to_address"=>{"name"=>"Address_1431693296"}, "justification"=>nil, "attachments"=>[], "requisition_lines"=>[{"description"=>"Cleaning Services for Building A", "line_num"=>1, "need_by_date"=>2010-09-23 07:00:00 UTC, "source_part_num"=>nil, "supp_aux_part_num"=>nil, "unit_price"=>#<BigDecimal:a60520c,'0.3E4',9(18)>, "supplier"=>{"name"=>"amazon.com"}, "account"=>{"code"=>"SF-Marketing-Indirect", "account_type"=>{"name"=>"Ace Corporate"}}, "currency"=>{"code"=>"USD"}, "payment_term"=>{"code"=>"Net 30"}, "shipping_term"=>{"code"=>"Standard"}, "commodity"=>{"name"=>"Marketing-Services"}}]}}
mystruct = DeepStruct.new hash
mystruct.requisition_header.requested_by.login # => coupasupport
mystruct.requisition_header.to_h # => {"requested_by"=>{"login"=>"coupasupport"}
You could use BasicObject#method_missing:
Code
class Hash
def method_missing(key,*args)
(args.empty? && key?(key)) ? self[key] : super
end
end
Example
hash = { animals: {
pets: { dog: "Diva", cat: "Boots", python: "Stretch" },
farm: { pig: "Porky", chicken: "Little", sheep: "Baa" }
},
finishes: {
tinted: { stain: "Millers", paint: "Oxford" },
clear: { lacquer: "Target", varnish: "Topcoat" }
}
}
hash.finishes.tinted.stain
#=> "Millers
hash.animals.pets.cat
#=> "Boots"
hash.animals.pets
#=> {:dog=>"Diva", :cat=>"Boots", :python=>"Stretch"}
hash.animals
#=> {:pets=>{:dog=>"Diva", :cat=>"Boots", :python=>"Stretch"},
# :farm=>{:pig=>"Porky", :chicken=>"Little", :sheep=>"Baa"}}
Reader challenge
There is a potential "gotcha" with this approach. I leave it to the reader to identify it. My example contains a clue. (Mind you, there may be other problems I haven't thought of.)

visiting hash with keys from array

I have a big hash with lots of nested key value pairs.
Eg.
h = {"foo" => {"bar" => {"hello" => {"world" => "result" } } } }
Now I want to access result and I have keys for that in array in proper sequence.
keys_arr = ["foo", "bar", "hello", "world"]
The motive is clear, I want to do following:
h["foo"]["bar"]["hello"]["world"]
# => "result"
But I don't know how to do this. I am currently doing:
key = '["' + keys_arr.join('"]["') + '"]'
eval("h"+key)
# => "result"
Which looks like a hack. Also it greatly reduces my ability to work with hash in real environment.
Please suggest alternate and better ways.
Using Enumerable#inject (or Enumerable#reduce):
h = {"foo" => {"bar" => {"hello" => {"world" => "result" } } } }
keys_arr = ["foo", "bar", "hello", "world"]
keys_arr.inject(h) { |x, k| x[k] }
# => "result"
UPDATE
If you want to do something like: h["foo"]["bar"]["hello"]["world"] = "ruby"
innermost = keys_arr[0...-1].inject(h) { |x, k| x[k] } # the innermost hash
innermost[keys_arr[-1]] = "ruby"
keys_arr.inject(h, :[])
will do
Another way:
h = {"foo" => {"bar" => {"hello" => {"world" => 10 } } } }
keys = ["foo", "bar", "hello", "world"]
result = h
keys.each do |key|
result = result[key]
end
puts result #=>10
If the key may not exist, see here:
Dealing with many [...] in Ruby

ruby db result set to array in a hash in a hash

I have a db query which returns results like:
db_result.each {|row| puts row}
{"IP"=>"1.2.3.4","Field1"=>"abc","Field2"=>"123"}
{"IP"=>"1.2.3.4","Field1"=>"abc","Field2"=>"234"}
{"IP"=>"1.2.3.4","Field1"=>"bcd","Field2"=>"345"}
{"IP"=>"3.4.5.6","Field1"=>"bcd","Field2"=>"456"}
{"IP"=>"3.4.5.6","Field1"=>"bcd","Field2"=>"567"}
And want to put it into a hash like:
{
"1.2.3.4" => {
"abc" => ["123", "234"],
"bcd" => "345"
},
"3.4.5.6" => {
"bcd" => ["456", "567"]
}
}
What I am currently doing is:
result_hash = Hash.new { |h, k| h[k] = {} }
db_result.each do |row|
result_hash[row["IP"]] = Hash.new { |h, k| h[k] = [] } unless result_hash.has_key? row["IP"]
result_hash[row["IP"]][row["Field1"]] << row["Field2"]
end
Which works, however was wondering if there is a neater way.
Consider this a peer-review. As a recommendation for processing and maintenance...
I'd recommend the data structure you want be a little more consistent.
Instead of:
{
"1.2.3.4" => {
"abc" => ["123", "234"],
"bcd" => "345"
},
"3.4.5.6" => {
"bcd" => ["456", "567"]
}
}
I'd recommend:
{
"1.2.3.4" => {
"abc" => ["123", "234"],
"bcd" => ["345"]
},
"3.4.5.6" => {
"abc" => [],
"bcd" => ["456", "567"]
}
}
Keep the same keys in each sub-hash, and make the values all be arrays. The code for processing that overall hash will be more straightforward and easy to follow.
I agree with Michael, there is nothing wrong with your method. The intent behind the code can be easily seen.
If you want to get fancy, here's one (of many) ways to do it:
x = [
{"IP"=>"1.2.3.4","Field1"=>"abc","Field2"=>"123"},
{"IP"=>"1.2.3.4","Field1"=>"abc","Field2"=>"234"},
{"IP"=>"1.2.3.4","Field1"=>"bcd","Field2"=>"345"},
{"IP"=>"3.4.5.6","Field1"=>"bcd","Field2"=>"456"},
{"IP"=>"3.4.5.6","Field1"=>"bcd","Field2"=>"567"}
]
y = x.inject({}) do |result, row|
new_row = result[row["IP"]] ||= {}
(new_row[row["Field1"]] ||= []) << row["Field2"]
result
end
I think this should yield the same time complexity as your method.

How to change format of nested hashes

I'm looking for a solution how to write the format function which will take a string or nested hash as an argument and return the flatten version of it with the path as a key.
arg = "foo"
format(arg) # => { "hash[keys]" => "foo" }
arg = {:a => "foo", :b => { :c => "bar", :d => "baz" }}
format(arg) # => { "hash[keys][a]" => "foo", "hash[keys][b][c]" => "bar", "hash[keys][b][d]" => "baz" }
def hash_flatten h
h.inject({}) do |a,(k,v)|
if v.is_a?(Hash)
hash_flatten(v).each do |sk, sv|
a[[k]+sk] = sv
end
else
k = k ? [k] : []
a[k] = v
end
a
end
end
def format h
if h.is_a?(Hash)
a = hash_flatten(h).map do |k,v|
key = k.map{|e| "[#{e}]"}.join
"\"event[actor]#{key}\" => \"#{v}\""
end.join(', ')
else
format({nil => h})
end
end
arg = "sth"
puts format(arg)
# => "event[actor]" => "sth"
arg = {:a => "sth", :b => { :c => "sth else", :d => "trololo" }}
puts format(arg)
# => "event[actor][a]" => "sth", "event[actor][b][c]" => "sth else", "event[actor][b][d]" => "trololo"

Resources