Hash Parsing with Ruby - ruby

I have the array of hash as shown below:
#line_statuses = [
{:name=>"1", :status=>"online"},
{:name=>"2", :status=>"online"}
]
I'd like to parse each hash inside of #line_statuses array so that I can print out the name and status as shown below.
1: online
2: online
How would I go about doing this?

Technically your #line_statuses variable is an array, so what you have is an array of hashes. In Ruby, to iterate over an array we use the .each method. Then, in each iteration, we can access the values of the hash using the defined keys:
#line_statuses.each do |hash|
puts hash[:name]
puts hash[:status]
end

So simple...:
#line_statuses.each do |line_status|
puts "#{line_status[:name]}: #{line_status[:status]}"
end

try #line_statuses.each{|i| puts i[:name],i[:status]}

Pretty simple:
#line_statuses.each { |line_status| puts "#{line_status[:name]}: #{line_status[:status]}" }

Related

Unpacking a Hash in ruby

I wonder if there is any method which can unpack the hashes inside an array. For example
array = [{:code=>"404"}, {:code=>"302"}, {:code=>"200"}]
After unpacking it should give
array = [code: "404", code: "302", code: "200"]
I have tried using flat_map as follows
array.flat_map { |h| h.values } & array.flat_map { |h| h.keys }
But these are 2 separate arrays.
For your updated version of the question, the answer's very straightforward:
array.reduce(&:merge)
reduce is a method to iterate through an object and accumulate the values in some way. Using &:merge is shorthand for merging your hashes together.
The result of this gives you:
# => {:code1=>"404", :code2=>"302", :code3=>"200"}
Your expected output [code: "404", code: "302", code: "200"] is not a valid ruby to declare an array having three elements. The actual outcome from the above would be an array having one single element:
[code: "404", code: "302", code: "200"]
(pry):39: warning: key :code is duplicated and overwritten on line 39
(pry):39: warning: key :code is duplicated and overwritten on line 39
#⇒ [{ :code => "200" }]
That is because one might omit curly brackets around hash when it introduces no ambiguity.
The best you can get is:
array.each_with_object({}) do |h, acc|
acc.merge!(h) { |_, v1, v2| [*v1, v2] }
end
#⇒ {:code=>["404", "302", "200"]}
Here you go:
array = [{:code=>"404"}, {:code=>"302"}, {:code=>"200"}]
[array.last] # => [{:code=>"200"}]
Now that you have changed your question, it has become clear that you basically just want to merge all the hashes into one hash. You could do it with this simple loop:
m = {}
array.each do |h|
m.merge!(h)
end
Ruby also provides a fancy way to do the same thing in one line:
array.each_with_object({}) { |h, m| m.merge!(h) }
Oh yeah, I almost forgot. You wrote your desired output with square brackets, which technically means that you are asking for an array that contains a hash. The code above just gives you a simple hash. In Ruby, we write arrays with square brackets and hashes with curly brackets, but the curly ones can be omitted in some cases. There is no obvious reason to throw your hash into an array that just has one element, but if you really want to do that, then you can of course do it like this, assuming m holds the merged hash:
weird_array = [m]

In Ruby, group_by where I know there's only 1 element per group

I have a CSV file where one column is a primary key. When I do this:
CSV.read(ARGV[0], headers: true).group_by {|r| r['myKey']}
I get a hash table from key to a list of rows, where the list is always length 1.
Is there a version of group_by which asserts that there's only a single value per key, and creates a hash from key to that single value?
Failing that, is there something like .first which asserts that there's exactly one element in the array/enumerable? I like my scripts to fail when my assumptions are wrong, rather than silently return the wrong thing.
If you use Rails you can use index_by method.
If you know the values r['myKey'] are unique, there's no point in using group_by. As I understand the question, you could do this:
rows = CSV.read(ARGV[0], headers: true)
Hash[rows.map { |r| r['myKey'] }.zip(rows)]
In Ruby 2.0+ the second row could be written:
rows.map { |r| r['myKey'] }.zip(rows).to_h
No. I don't believe there is. But you can solve your problem with each_with_object like so:
CSV.
read(ARGV[0], headers: true).
each_with_object({}) do |r, hash|
key = r['myKey']
value = r
hash[key] = value
end
It's a shame Ruby doesn't have this. Here's what I decided to go on, based on Humza's answer:
module Enumerable
def group_by_uniq
each_with_object({}) do |value, hash|
key = yield value
raise "Multiple values for key \"{key}\"!" unless ! hash.key?(key)
hash[key] = value
end
end
end
If you use your code in you first example you can run this code to check that all hashes are of length 1:
raise 'multiple entries per key!' unless my_hash.values.any?{|val| val.size!=1}
IF you can get the keys into an array you can check that they do not iclude duplicates by:
raise 'multiple entries per key!' unless my_keys.uniq.size == my_keys.size

Comparing values in two hashes

I am having trouble in comparing values in two hashes, getting the error "Can't convert String into Integer".
First hash has values captured from a web page using the method "capture_page_data(browser)" and the second hash has data parsed from a report.
Code looks like below:
# Open the web application
# Navigate to a specific page and capture page data
loan_data = Hash.new
loan_data = capture_page_data(browser)
Second hash has values captured from a report generated from the web application.
Code looks like below:
#report_data[page] = Hash.new
# we have written some logic to parse the data from the report into hash variable
Now I am trying to compare the values in theses two hashes to ensure the data in report is matching with the data in application using below code which is giving me the error "Can't convert String into Integer".
loan_data.map{|ld| ld['MainContent_cphContent_LoanOverViewGeneralInfoCtrl_lblRelName']} &
#report_data.map{|rd| rd['Relationship']}
Please help me out in resolving this issue.
Regards,
Veera.
Hash#map iterates through the hash like it was an array of key/value pairs.
{a:1,b:2}.map{|x| puts x.inspect }
# prints
# [:a,1]
# [:b,2]
{a:1,b:2}.map{|k,v| puts "#{k} => #{v}" }
# prints
# a => 1
# b => 2
It applies the block you provide to each pair and collects the results into a new array.
result = {a:1,b:2}.map{|k,v| "#{k} => #{v}" }
puts result.inspect
# prints
# [ "a => 1", "b => 2" ]
I would guess what you are trying to do is compare a single key from each array... in which case...
if loan_data[:id][:span]['MainContent_cphContent_LoanOverViewGeneralInfoCtrl_lblR‌​elName'] == #report_data[1]['Relationship']
log_message("pass")
else
log_message("fail")
end
might be what you are trying to do.. but I am only guessing.
It all depends on the shape of your data.
If you inspect the ld variable inside your block, you will find that it is an array. You can get an element of it with ld[0] or ld[1], but ld[string] does not make sense and results in the exception you are seeing. The ld array will actually be an array with two elements: key and value.
Thanks for your suggestions.. but I found a different solution to compare a single key from two hashes/Arrays using the below code which worked fine.
string_equals?(loan_data[:id][:span]['MainContent_cphContent_LoanOverViewGeneralInfoCtrl_lblRelName'], #report_data[1]['Relationship'] )
Thanks,
Veera.
It's best to debug the content of loan_data and #report_data directly, but you can try .to_sym to convert the key into symbol.
loan_data.map{|ld| ld['MainContent_cphContent_LoanOverViewGeneralInfoCtrl_lblRelName'.to_sym]} &
#report_data.map{|rd| rd['Relationship'.to_sym]}

Remove the '-' dividers in JSON keys in Ruby

I'm trying to read some JSON data from the Tumblr API.
I'm using the Hashie gem to read the values as object properties. This should make reading easier/cleaner.
it turns something like this:
data['post']['title']
into this:
data.post.title
Unfortunately there are some keys showing up with a '-' as divider between like this:
regular-title: Mijn eerste post
format: html
regular-body: <p>post</p>
therefore i cannot use post.regular-title. Is there a way to replace all the minus(-) symbols into underscores(_)?
This will do it:
def convert_object(data)
case data
when Hash
data.inject({}) do |h,(k,v)|
h[(k.respond_to?(:tr) ? k.tr('-', '_') : k)] = convert_object(v)
h
end
when Array
data.map { |i| convert_object(i) }
else
data
end
end
You can use it like this:
convert_object(JSON.parse('{"something-here":"value","otherkey":{"other-key":"value-value"}}'))
Karaszi Istvan helped me a lot with the solution. I added the check for an array in the hash. This way hashes in arrays in the hash will get underscored too.
def convert_hash(hash)
case hash
when Hash
hash.inject({}) do |h,(k,v)|
h[k.tr('-', '_')] = convert_hash(v)
h
end
when Array
array = hash
number = 0
array.each do
array[number] = convert_hash(array[number])
number += 1
end
array
else
hash
end
end
I don't know why i added the 'number' as iterator. Somehow hash.each didn't work.

TypeError: can't convert String into Integer

I have code:
class Scene
def initialize(number)
#number = number
end
attr_reader :number
end
scenes = [Scene.new("one"), Scene.new("one"), Scene.new("two"), Scene.new("one")]
groups = scenes.inject({}) do |new_hash, scene|
new_hash[scene.number] = [] if new_hash[scene.number].nil?
new_hash[scene.number] << scene
end
When I'm lauching it I get error:
freq.rb:11:in `[]': can't convert String into Integer (TypeError)
from freq.rb:11:in `block in <main>'
from freq.rb:10:in `each'
from freq.rb:10:in `inject'
from freq.rb:10:in `<main>'
If I change scenes to:
scenes = [Scene.new(1), Scene.new(1), Scene.new(2), Scene.new(1)]
the problem dissapear.
Why I get error message in the first case? Why Ruby decide to convert scene.number from String to Integer?
And one additional question about the 'inject' method. When Ruby initialize the 'new_hash' variable and how can Ruby know the type of this variable?
try:
groups = scenes.inject({}) do |new_hash, scene|
new_hash[scene.number] = [] if new_hash[scene.number].nil?
new_hash[scene.number] << scene
new_hash
end
Ruby takes the empty hash passed into inject() and sets new_hash to that. When the block ends the return value gets used to initialize new_hash the next time through, i.e., new_hash keeps accumulating the result of the block.
In your original code you were not returning the hash but an array (new_hash[scene.number] is an array) and the next loop through Ruby complained because new_hash[scene.number] was trying to do a lookup into the array with a string value, hence the error you got.
Z.E.D.'s right. See Jay Fields' Thoughts: Ruby: inject for a good explanation of inject by example.
As presented, your block returns an array. So the new_hash in |new_hash, scene| ends up being that array. When Ruby tries to find the array index 'one', it throws the error because 'one' is a String, not an Integer.
All you need to do is return new_hash as Z.E.D. showed, and you'll get something like this:
{
"two" => [
#<Scene:0x101836470 #number="two">
],
"one" => [
#<Scene:0x101836510 #number="one">,
#<Scene:0x1018364c0 #number="one">,
#<Scene:0x101836420 #number="one">
]
}
Why not use group_by which is probably exactly what you try to accomblish?
groups = scenes.group_by(&:number)
# => {"two"=>[#<Scene:0xb728ade0 #number="two">],
# "one"=>
# [#<Scene:0xb728ae30 #number="one">,
# #<Scene:0xb728ae08 #number="one">,
# #<Scene:0xb728ada4 #number="one">]}
inject is a folding operation and not exactly what you want. At least it's cumbersome to use in this way. merge with a block would probably be appropriate if you want to apply some algorithm during merging or grouping.
Also, to explain 'how can Ruby know the type of this variable' and why it tries to 'convert String into Integer' you might want to revise: Ruby variables and dynamic typing.
I know an answer is accepted for this question, but I can't help but post my answer.
groups = scenes.inject({}) { |nh, s| nh.tap {|h| (h[s.number] ||= []) << s } }

Resources