Turning a multi-dimensional array into a hash without overwriting values - arrays

I have a multi-dimensional array such as:
array = [["stop", "halt"],["stop", "red"],["go", "green"],["go","fast"],["caution","yellow"]]
And I want to turn it into a hash like this:
hash = {"stop" => ["halt","red"], "go" => ["green","fast"], "caution" => "yellow"}
However, when I array.to_h , the values overwrite one another and I get:
hash = {"stop" => "red", "go" => "fast", "caution" => "yellow"}
How do I get the desired array?

This is one way. It uses Enumerable#each_with_object and the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged.
array << ["stop", "or I'll fire!"]
array.each_with_object({}) { |(f,l),h|
h.update(f=>l) { |_,ov,nv| ov.is_a?(Array) ? ov << nv : [ov, nv] } }
#=> {"stop"=>["halt", "red", "or I'll fire!"],
# "go"=>["green", "fast"],
# "caution"=>"yellow"}
The code is simplified if you want all values in the returned hash to be arrays (i.e., "caution"=>["yellow"]), which is generally more convenient for subsequent calculations:
array.each_with_object({}) { |(f,l),h| h.update(f=>[l]) {|_,ov,nv| ov+nv }}
#=> {"stop"=>["halt", "red", "or I'll fire!"],
# "go"=>["green", "fast"],
# "caution"=>["yellow"]}

One way to do it:
array.inject({}) {|r, (k, v)| r[k] &&= [*r[k], v]; r[k] ||= v; r }
That's pretty messy though. Written out, it looks like this:
def to_hash_with_duplicates(arr)
{}.tap do |r|
arr.each do |k, v|
r[k] &&= [*r[k], v] # key already present, turn into array and add value
r[k] ||= v # key not present, simply store value
end
end
end
Edit: Thinking a bit more, #cary-swoveland's update-with-block solution is better, because it handles nil and false values correctly.

Related

Ruby - Print only the values of dubplicate hash keys in an array of hashes

I have created an array of hashes from data ive to pull in from an xml file. Problem is, some of the hash keys in the array are duplicates and id like to pull just the values. For example, the code below outputs the following:
{"server_host"=>"hostone", "server_type"=>"redhat", "server_name"=>"RedhatOne"}
{"server_host"=>"hostone", "server_type"=>"windows", "server_name"=>"WinOne"}
and i'd like to be able print out this:
{"server_host"=>"hostone", "server_type"=>"redhat", "server_name"=>"RedhatOne"}
"server_type"=>"windows", "server_name"=>"WinOne"}
I think i need to create another array based on duplicate keys but what i am trying below is not working:
def parse_xml_file(filename)
require 'nokogiri'
xmlSource = File.read(filename)
parsedXml = Nokogiri::XML(xmlSource)
hostArray = Array.new
parsedXml.xpath("/New/Server").each do |srvNode|
hostNode = srvNode.at_xpath("Host")
hostArray << {"server_name"=>srvNode["Name"],
"server_type"=>srvNode["Type"], "server_host"=>hostNode["Address"] }
grouped = hostArray.group_by{|row| [row[:server_host]]}
filtered = grouped.values.select { |a| a.size > 1 }.flatten
end
Assuming you have a variable hash_arr which contains your duplicated hashes, here is some code that should get you pretty close to where you want to be. It's not optimized, but it's simple enough to understand:
hash_arr.group_by { |h| h["server_host"] }.each do |host_name, values|
puts "Server Host: #{host_name}"
values.each do |val|
val.delete("server_host")
puts val
end
end
prints out:
Server Host: hostone
{"server_type"=>"redhat", "server_name"=>"RedhatOne"}
{"server_type"=>"windows", "server_name"=>"WinOne"}
Or if you just the values per group without associating them across hashes:
hash_arr =[{"server_host"=>"hostone", "server_type"=>"redhat", "server_name"=>"RedhatOne"}, {"server_host"=>"hostone", "server_type"=>"windows", "server_name"=>"WinOne"}]
merged_hash = {}
hash_arr.each do |hash|
hash.each do |k, v|
merged_hash[k] ||= []
merged_hash[k] << v
end
end
merged_hash.values.each(&:uniq!)
And then the output:
[9] pry(main)> merged_hash
=> {"server_host"=>["hostone"], "server_type"=>["redhat", "windows"], "server_name"=>["RedhatOne", "WinOne"]}
This will get you the shared values:
shared = hash1.keep_if { |k, v| hash2.key? k }
And them you could print that however you like. Don't know if you want to print the keys, values, or both, but however you like:
shared.each_pair { |k, v| print k, v }
You could obviously merge these two snippets into one command, but for the sake of clarity, they are 2.
EDIT:
Just noticed you wanted as an array. If you wanted just values:
array = hash1.keep_if { |k, v| hash2.key? k }.values
Thanks for the advice - i've tried this :
shared = Hash.new
grouped = hostArray.group_by{|row| [row[:server_host]]}
filtered = grouped.values.select { |a| a.size > 1 }.flatten
filtered.each do |element|
element.each do |key, value|
shared = element.keep_if { |k, v| element.key? k }
end
shared.each_pair { |k, v| print k," ", v, "\n" }
end
but this output is still incorrect - i think i've referenced 'hash2' wrongly? is that correct?

Ruby -- convert a nested hash to a multidimensional array

I have a hash which is named h. I want to store the contents in a multidimensional array named ar. I am getting the error no implicit conversion from nil to integer.
Here is my code:
h = {"bob" => {email: "abc" , tel: "123"} , "daisy" => {email: "cab" , tel: "123456"}}
keys = h.keys
l = h.length
ar = Array.new(l) { Array.new(3) }
for i in 0..l-1
ar[[2][i]] = keys[i]
ar[[1][i]] = h[keys[i]][:email]
ar[[0][i]] = h[keys[i]][:tel]
end
puts ar.to_s
The desired output is:
[[email_1, email_2, ..][tel_1, tel_2, ..][name_1, name_2, ..]]
For example:
[["abc", "cab"] , ["123", "123456"] , ["bob", "daisy"]]
This is the way I would handle this:
h.values.each_with_object({}) do |h,obj|
obj.merge!(h) { |_k,v1,v2| ([v1] << v2).flatten }
end.values << h.keys
#=> [["abc", "cab"], ["123", "123456"], ["bob", "daisy"]]
First grab all the values (as Hashes)
loop through them with an accumulator ({})
merge! the values into the accumulator and on conflict append them to an array
return the values from the accumulator
then append the original keys
This is less explicit than #mudasobwa's answer and relies on the order of the first value to determine the output. e.g. if :tel came before :email the first 2 elements would have a reversed order
[2][i] returns nil for i > 0. ar[nil] raises the exception.
Here is what you do:
arr = h.map { |k, v| [v[:email], v[:tel], k] }.reduce(&:zip)
To make your code work:
Change
ar = Array.new(l) { Array.new(3) }
To
ar = Array.new(3) { Array.new(l) }
Change
ar[[2][i]] = keys[i]
ar[[1][i]] = h[keys[i]][:email]
ar[[0][i]] = h[keys[i]][:tel]
To
ar[2][i] = keys[i]
ar[1][i] = h[keys[i]][:email]
ar[0][i] = h[keys[i]][:tel]
What you mostly should do is to stop writing PHP code with Ruby syntax. Here it’s how is to be done in Ruby:
h.map { |k, v| [v[:email], v[:tel], k] }.reduce(&:zip)
or, even better, if you are certain of elements order in nested hashes:
h.map { |k, v| [*v.values, k] }.reduce(&:zip).map(&:flatten)
All the methods map, reduce and zip are thoroughly described in the documentation.
h.map { |k, v| [*v.values_at(:email, :tel), k] }.transpose
#=> [["abc", "cab"], ["123", "123456"], ["bob", "daisy"]]
The intermediate calculation is as follows.
h.map { |k, v| [*v.values_at(:email, :tel), k] }
#=> [["abc", "123", "bob"], ["cab", "123456", "daisy"]]

Using gsub in array of hashes

I want to remove the spaces in the key value in the hashes
output = [
{"first name"=> "george", "country"=>"Australia"},
{"second name"=> "williams", "country"=>"South Africa"},
{"first name"=> "henry", "country"=>"US"}]
I was able to manage when only one hash was there inside the array with the following code
Array.wrap({}.tap do |hash|
output.each do |key|
key.each do |k, v|
hash[k.gsub(" ","_")] = v
end
end
end)
Please help me to modify the array containing more than one hash.
Note: the output value is dynamic that we cannot hardcode the hash key in the code.
If hash is not nested - you can simply
output.map{|h| h.each_pair.map{|k,v| [k.gsub(' ', '_'), v]}.to_h }
Here's code that will change the spaces to underscores for each key in a hash:
output.flat_map { |h| h.map { |key, v| { key.gsub(" ", "_") => v } } }
=> [{"first_name"=>"george"}, {"country"=>"Australia"}, {"second_name"=>"williams"}, {"country"=>"South Africa"}, {"first_name"=>"henry"}, {"country"=>"US"}]
You cannot modify a hash's keys. You must remove the unwanted key and add a new one. Here's a way of doing both operations in one step (see the doc Hash#delete):
def convert(h)
h.keys.each { |k| (h[k.tr(' ','_')] = h.delete(k)) if k =~ /\s/ }
h
end
Hence:
output.map { |h| convert h }
#=> [{"country"=>"Australia", "first_name"=>"george"},
# {"country"=>"South Africa", "second_name"=>"williams"},
# {"country"=>"US", "first_name"=>"henry"}]
I've used the method String#tr to convert spaces to underscores, but you could use String#gsub as well. Also, you could write k.include?(' ') rather than k =~ /\s/.

Return unique values of an array without using `uniq`

For a challenge, I'm trying to return the unique values of an array without using uniq. This is what I have so far, which doesn't work:
def unique
unique_arr = []
input_arr.each do |word|
if word != unique_arr.last
unique_arr.push word
end
end
puts unique_arr
end
input = gets.chomp
input_arr = input.split.sort
input_arr.unique
My reasoning here was that if I sorted the array first before I iterated through it with each, I could push it to unique_arr without repetition being a possibility considering if it's a duplicate, the last value pushed would match it.
Am I tackling this the wrong way?
Yes, you are making at least two mistakes.
If you want to call it as input_arr.unique with input_arr being an array, then you have to define the method on Array. You have input_arr within your method body, which comes from nowhere.
puts in the last line of your code outputs to the terminal, but makes the method return nil, which makes it behave differently from uniq.
It can be fixed as:
class Array
def unique
unique_arr = []
each do |word|
unique_arr.push(word) unless unique_arr.last == word
end
unique_arr
end
end
A unique array? That sounds like a Set to me:
require 'set'
Set.new([1,2,3,2,3,4]).to_a
#=> [1,2,3,4]
Here's a concise way to do it that doesn't explicitly use functionality from another class but probably otherwise misses the point of the challenge:
class Array
def unique
group_by(&:itself).keys
end
end
I try this three options. Just for challenge
class Array
def unique
self.each_with_object({}) { |k, h| h[k] = k }.keys
end
def unique2
self.each_with_object([]) { |k, a| a << k unless a.include?(k) }
end
def unique3
arr = []
self.map { |k| arr << k unless arr.include?(k) }
arr
end
end
Here is one more way to do this:
uniques = a.each.with_object([]) {|el, arr| arr << el if not arr.include?(el)}
That's so easy if you see it this way:
a = [1,1,2,3,4]
h = Hash.new
a.each{|q| h[q] = q}
h.values
and this will return:
[1, 2, 3, 4]

Add key value pair to Array of Hashes when unique Id's match

I have two arrays of hashes
sent_array = [{:sellersku=>"0421077128", :asin=>"B00ND80WKY"},
{:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
{:sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
active_array = [{:price=>39.99, :asin1=>"B00ND80WKY"},
{:price=>7.99, :asin1=>"B00YSN9QOG"},
{:price=>10, :asin1=>"B00HXZLB4E"}]
I want to loop through sent_array, and find where the value in :asin is equal to the value in :asin1 in active_array, then copy the key & value of :price to sent_array. Resulting in this:
final_array = [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99},
{:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
{:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
I tried this, but I get a TypeError - no implicit conversion of Symbol into Integer (TypeError)
sent_array.each do |x|
x.detect { |key, value|
if value == active_array[:asin1]
x[:price] << active_array[:price]
end
}
end
For reasons of both efficiency and readability, it makes sense to first construct a lookup hash on active_array:
h = active_array.each_with_object({}) { |g,h| h[g[:asin1]] = g[:price] }
#=> {"B00ND80WKY"=>39.99, "B00YSN9QOG"=>7.99, "B00HXZLB4E"=>10}
We now merely step through sent_array, updating the hashes:
sent_array.each { |g| g[:price] = h[g[:asin]] if h.key?(g[:asin]) }
#=> [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99},
# {:sellersku=>"0320248102", :asin=>"B00WTEF9FG"},
# {:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
Retrieving a key-value pair from a hash (h) is much faster, of course, than searching for a key-value pair in an array of hashes.
This does the trick. Iterate over your sent array and attempt to find a record in your active_array that has that :asin. If you find something, set the price and you are done.
Your code I believe used detect/find incorrectly. What you want out of that method is the hash that matches and then do something with that. You were trying to do everything inside of detect.
sent_array.each do |sent|
item = active_array.find{ |i| i.has_value? sent[:asin] }
sent[:price] = item[:price] if item
end
=> [{:sellersku=>"0421077128", :asin=>"B00ND80WKY", :price=>39.99}, {:sellersku=>"0320248102", :asin=>"B00WTEF9FG"}, {:sellersku=>"0324823180", :asin=>"B00HXZLB4E", :price=>10}]
I am assuming second element of both sent_array and active_array has B00WTEF9FG as asin and asin1 respectively. (seeing your final result)
Now:
a = active_array.group_by{|a| a[:asin1]}
b = sent_array.group_by{|a| a[:asin]}
a.map { |k,v|
v[0].merge(b[k][0])
}
# => [{:price=>39.99, :asin1=>"B00ND80WKY", :sellersku=>"0421077128", :asin=>"B00ND80WKY"}, {:price=>7.99, :asin1=>"B00WTEF9FG", :sellersku=>"0320248102", :asin=>"B00WTEF9FG"}, {:price=>10, :asin1=>"B00HXZLB4E", :sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
Why were you getting TypeError?
You are doing active_array[:asin1]. Remember active_array itself is an Array. Unless you iterate over it, you cannot look for keys.
Another issue with your approach is, you are using Hash#detect
find is implemented in terms of each. And each, when called on a
Hash, returns key-value pairs in form of arrays with 2 elements
each. That's why find returns an array.
source
Same is true for detect. So x.detect { |key, value| .. } is not going to work as you are expecting it to.
Solution without assumption
a.map { |k,v|
b[k] ? v[0].merge(b[k][0]) : v[0]
}.compact
# => [{:price=>39.99, :asin1=>"B00ND80WKY", :sellersku=>"0421077128", :asin=>"B00ND80WKY"}, {:price=>7.99, :asin1=>"B00YSN9QOG"}, {:price=>10, :asin1=>"B00HXZLB4E", :sellersku=>"0324823180", :asin=>"B00HXZLB4E"}]
Here since asin1 => "B00ND80WKY" has no match, it cannot get sellersku from other hash.

Resources