Using gsub in array of hashes - arrays

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/.

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"]]

Deleting specific string lines of items in array Ruby

I have an array of 10 items containing of a several lines string like
one string
two string
some string
any string
I want to delete lines containing words some and two. I made code like that:
search_text_domain = %r{some|two}
groups_data.each do |line|
line.each_line do |num|
domain_users_name << (num) unless num =~ search_text_domain
end
end
It works fine but it puts all lines to one big array like
domain_users_name = ["one string", "any string", "big string", "another_s....] and I want tu put it in array of arrays like
domain_users_name = [["one string", "any string"], ["big string", ""another_s...."], [........
I need version that permanently modify groups_data array. Any ideas?
input = ["one string\ntwo string\nsome string\nany string",
"one string\ntwo string\nsome string\nany string"]
input.map { |a| a.split("\n").reject { |e| e =~ %r{some|two} } }
# or
# input.map { |a| a.each_line.map(&:strip).reject { |e| e =~ %r{some|two} } }
# or (smiley-powered version, see the method’s tail)
# input.map { |a| a.each_line.map(&:strip).reject(&%r{some|two}.method(:=~)) }
#⇒ [["one string", "any string"], ["one string", "any string"]]
So you want to delete a group if one of the group elements matches the filter regexp?
groups = [['some', 'word'], ['other', 'word'], ['unrelated', 'list', 'of', 'things']]
filter = %r{word|some}
filtered = groups.delete_if do |group|
group.any? do |word|
word =~ filter
end
end
p filtered
Does this do what you want?

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]

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

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.

Resources