I have this structure of data in a hash:
[{"name"=>"Peter", "surname"=>"Green"}, {"name"=>"Jane", "surname"=>"Miller"}]
But when I try to work with this hash, for example:
puts hash.count # returns nothing
hash.each do |data|
puts data.name # => undefined method `name' for #<Hash:0x00000104bcf9f8>
end
What am I missing?
Array#count without an argument should return the number of elements, but it is more natural to use length or size. And Hash does not have a method name.
puts hash.length
hash.each do |data|
puts data["name"]
end
By the way, what you refer to as hash is actually an array, and is confusing.
Related
I'm working on processing a response of a Webhook from GitHub. The response contains a hash web_hook_response that looks like this
{:commits=>
{:modified=>
["public/en/landing_pages/dc.json",
"draft/en/landing_pages/careers/ac123.json"]
}
}
Now I have a function that processes this hash.
modified_or_deleted_files = []
web_hook_response[:commits].map do |commit|
modified_or_deleted_files << commit[:removed] << commit[:modified]
end
I get this error
TypeError: no implicit conversion of Symbol into Integer
I tried to find out the value of commit when it's inside the map block and this is what got printed
[:modified,
["public/en/landing_pages/dc.json",
"draft/en/landing_pages/careers/ac123.json"]]
Why is the modified hash converting into an array of a symbol and an array inside the map block? I can't explain why this is happening. Can anyone explain why this is happening?
Data
This is the hash you are given (simplified slightly).
web_hook_response = {
:commits => { :modified => ["public", "draft"] }
}
It has one key-value pair, the key being :commits and the value being the hash
{ :modified => ["public", "draft"] }
which itself has one key (:modified) and one value (["public", "draft"]).
Error
Try this (with my definition of web_hook_response):
web_hook_response[:commits].map do |commit|
puts "commit = #{commit}"
modified_or_deleted_files << commit[:removed] << commit[:modified] # line 397
end
# commit = [:modified, ["public", "draft"]]
# TypeError: no implicit conversion of Symbol into Integer
from (irb):397:in `[]'
from (irb):397:in `block in irb_binding'
from (irb):395:in `each'
from (irb):395:in `map'
Note that commit equals a key-value pair from the hash web_hook_response[:commits]. As you see, an attempt is made to compute
commit[:removed]
#=> [:modified, ["public", "draft"]][:removed]
which is the syntactic sugar form of the conventional expression
[:modified, ["public", "draft"]].[](:removed)
Since [:modified, ["public", "draft"]] is an array, Array#[] is an instance method of the class Array. (Yes, it's a funny name for a method, but that's what it is.) As explained in its doc, the method's argument must be an integer, namely, the index of an element of the array that is to to be returned. Therefore, when Ruby discovers that the argument is a symbol (:removed), she raises the exception, "no implicit conversion of Symbol into Integer".
Computing modified_or_deleted_files
Given the keys :commits and :modified we may extract the hash
h = web_hook_response[:commits]
#=> { :modified=>["public", "draft"] }
and from that extract the array
a = h[:modified]
#=> ["public", "draft"]
We would normally chain these two operations to obtain the array in one statement.
web_hook_response[:commits][:modified]
#=> ["public", "draft"]
It appears you wish to simply set the value of the variable modified_or_deleted_files to this array, so simply write the following.
modified_or_deleted_files = web_hook_response[:commits][:modified]
#=> ["public", "draft"]
web_hook_response[:commits] is a hash, not an array, so when you call map on it, the block parameter commit gets key-value pairs, which are arrays of size 2.
I think what you need is to concatenate 2 arrays. You can
modified_or_deleted_files = web_hook_response[:commits].slice(:modified, :removed).values.flatten
Your :commits is a hash. When iterating through hashes, you usually use two block arguments, one for the key and one for the value, for example:
{ :foo => 'bar' }.each do |key, value|
puts "#{key} = #{value}"
}
# outputs:
# foo = bar
When you only use one block argument you will get a key-value pair in an array:
{ :foo => 'bar' }.each do |pair|
puts pair.inspect
end
# outputs:
# [:foo, "bar"]
In your example you could just do:
commits = web_hook_response[:commits]
modified_or_deleted_files = Array(commits[:removed]) + Array(commits[:modified])
(The Array(...) is used to avoid an error if commits[:removed] or commits[:modified] is nil. Array(nil) returns an empty array, Array(an_array) returns the array)
Or if you want to get fancy with the enumerators, iterators and such:
modified_or_deleted_files = web_hook_response[:commits].
values_at(:modified, :removed).
compact.
reduce(:+)
Write a function that accepts a multi-dimensional container of any size and converts it into a one dimensional associative array whose keys are strings representing their value's path in the original container.
So { 'one' => {'two' => 3, 'four' => [ 5,6,7]}, 'eight'=> {'nine'=> {'ten'=>11}}}
would become
:
"{'one/two' => 3,'one/four/0' => 5, 'one/four/1' => 6, 'one/four/2' => 7, 'eight/nine/ten' : 11}"
I've gotten this so far... But am having a lot of issues. Any pointers to things I am overlooking?
def oneDimHash(hash)
if hash.is_a?(Fixnum)
puts "AHHH"
else
hash.each_pair do |key,value|
if value.is_a?(Hash)
#temp_key << key << '/'
oneDimHash(value)
elsif value.is_a?(Array)
value.each_with_index do |val,index|
puts index
#temp_key << "#{index}"
oneDimHash(val)
end
else
#temp_key << key
#result["#{#temp_key}"] = "#{value}"
#temp_key = ''
end
end
end
end
It's immediately suspect to me that you are using instance variables instead of method arguments / local variables. Very likely that is producing messed-up keys, at least. Supposing that the method signature cannot be modified, you can work around the need for additional arguments by delegating to a helper function. Perhaps I'd try an approach along these lines:
def oneDimHash(o)
oneDimHashInternal("", o, {})
end
def oneDimHashInternal(keyStem, o, hash)
if o.is_a? Hash
o.each_pair do |key, value|
oneDimHashInternal("#{keystem}/#{key}", value, hash)
end
elsif o.is_a? Array
# Work this out for yourself
else
# Store the (non-container) object in hash
# Work this out for yourself
end
hash
end
Note also that there are Enumerables that are neither Arrays nor Hashes. I don't know whether you need to account for such.
How about this?
def oneDimHash(obj,parent="")
unless obj.is_a?(Hash)
puts "AHHH" # or may be better: raise "AHHH"
else
obj.flat_map do |key,value|
combined_key = [parent,key.to_s].join '/'
case value
when Hash then oneDimHash(value,combined_key).to_a
when Array then value.each_with_index.map { |v,i| [combined_key+"/#{i}",v] }
else [ [combined_key,value] ]
end
end.to_h
end
end
I have a terribly nested Json response.
[[{:test=>[{:id=>1, :b=>{id: '2'}}]}]]
There's more arrays than that but you get the idea.
Is there a way to recursively search through and find all the items that have a key I need?
I tried using this function extract_list() but it doesn't handle arrays well.
def nested_find(obj, needed_keys)
return {} unless obj.is_a?(Array) || obj.is_a?(Hash)
obj.inject({}) do |hash, val|
if val.is_a?(Hash) && (tmp = needed_keys & val.keys).length > 0
tmp.each { |key| hash[key] = val[key] }
elsif val.is_a?(Array)
hash.merge!(obj.map { |v| nested_find(v, needed_keys) }.reduce(:merge))
end
hash
end
end
Example
needed_keys = [:id, :another_key]
nested_find([ ['test', [{id:1}], [[another_key: 5]]]], needed_keys)
# {:id=>1, :another_key=>5}
The following is not what I'd suggest, but just to give a brief alternative to the other solutions provided:
2.1.1 :001 > obj = [[{:test=>[{:id=>1, :b=>{id: '2'}}]}]]
=> [[{:test=>[{:id=>1, :b=>{:id=>"2"}}]}]]
2.1.1 :002 > key = :id
=> :id
2.1.1 :003 > obj.inspect.scan(/#{key.inspect}=>([^,}]*)[,}]/).flatten.map {|s| eval s}
=> [1, "2"]
Note: use of eval here is just for an example. It would fail/produce incorrect results on anything whose inspect value was not eval-able back to the same instance, and it can execute malicious code:
You'll need to write your own recursive handler. Assuming that you've already converted your JSON to a Ruby data structure (via JSON.load or whatnot):
def deep_find_value_with_key(data, desired_key)
case data
when Array
data.each do |value|
if found = deep_find_value_with_key value, desired_key
return found
end
end
when Hash
if data.key?(desired_key)
data[desired_key]
else
data.each do |key, val|
if found = deep_find_value_with_key(val, desired_key)
return found
end
end
end
end
return nil
end
The general idea is that given a data structure, you check it for the key (if it's a hash) and return the matching value if found. Otherwise, you iterate it (if it's an Array or Hash) and perform the same check on each of it's children.
This will find the value for the first occurrence of the given key, or nil if the key doesn't exist in the tree. If you need to find all instances then it's slightly different - you basically need to pass an array that will accumulate the values:
def deep_find_value_with_key(data, desired_key, hits = [])
case data
when Array
data.each do |value|
deep_find_value_with_key value, desired_key, hits
end
when Hash
if data.key?(desired_key)
hits << data[desired_key]
else
data.each do |key, val|
deep_find_value_with_key(val, desired_key)
end
end
end
return hits
end
irb> pp config
[{"file"=>"/var/tmp"},
{"size"=>"1024"},
{"modified"=>"03/28/2012"}]
=> nil
In the code,
config.each do |d|
# then how to break d into (k, v)???
end
config.each do |items|
items.each do |key, value|
# e.g. key="file", value="/var/tmp", etc.
end
end
Just do
config.each do |hash|
(k,v),_ = *hash
end
Inspired by #Arup's answer, here's a solution that doesn't require a extra, unused variable in the parallel assignment:
config.each do |hash|
key, value = hash.to_a[0]
end
to_a converts the hash into the same kind of array that you would get by using splat *hash, but you can actually index the first element of the array (i.e. the first key/value pair) with [0] this way, while trying to do so with splat (*hash) generates a syntax error (at least in Ruby version 2.1.1):
>> k,v = (*hash)[0]
SyntaxError: (irb):4: syntax error, unexpected ')', expecting '='
k,v = (*x)[0]
^
from c:/RailsInstaller/Ruby1.9.3/bin/irb:12:in `<main>'
>>
Of course, depending on what you're going to do with the key and value variables, it might make your code shorter and more readable to use one of these standard block constructs:
config.each do |hash|
hash.each { |key,value| puts "#{key}: #{value}" }
end
# or
config.each do |hash|
hash.each do |key,value|
puts "#{key}: #{value}"
end
end
Stuck on a Code Wars Challenge: Complete the solution so that it takes an array of keys and a default value and returns a hash with all keys set to the default value.
My answer results in a parse error:
def solution([:keys, :default_value])
return { :keys => " ", :default_value => " " }
end
Am I missing something to do with returning a hash key with all the keys set to the default value?
Do as below :
def solution(keys,default_val)
Hash[keys.product([default_val])]
end
solution([:key1,:key2],12) # => {:key1=>12, :key2=>12}
Read Array#product and Kernel#Hash.
I'd advise amending your solution to this:
def solution(keys, default_value)
hash = {}
keys.each do |key|
value = default_value.dup rescue default_value
hash[key] = value
end
hash
end
The dup is to work around the nasty case where default_value is a string and you then do e.g.:
hash[:foo] << 'bar'
… with your version, this would modify multiple values in place instead of a single one.