issue with using inject to convert array to hash - ruby

data_arr = [['dog', 'Fido'], ['cat', 'Whiskers'], ['fish', 'Fluffy']]
data_hash = data_arr.inject({}) do |hsh, v|
hsh[v[0]] = v[1]
hsh
end
Hi, why do I not need to initialize data_hash as an empty hash? And why do I have to add hsh in the last line if not it will result in an error.

why do I not need to initialize data_hash as an empty hash?
You do, implicitly. The value passed to inject, i.e. {} will become the initial value for hsh which will eventually become the value for data_hash. According to the documentation:
At the end of the iteration, the final value of memo is the return value for the method.
Let's see what happens if we don't pass {}:
If you do not explicitly specify an initial value for memo, then the first element of collection is used as the initial value of memo.
The first element of your collection is the array ['dog', 'Fido']. If you omit {}, then inject would use that array as the initial value for hsh. The subsequent call to hsh[v[0]] = v[1] would fail, because of:
hsh = ['dog', 'Fido']
hsh['cat'] = 'Whiskers'
#=> TypeError: no implicit conversion of String into Integer
why do I have to add hsh in the last line
Again, let's check the documentation:
[...] the result [of the specified block] becomes the new value for memo.
inject expects you to return the new value for hsh at the end of the block.
if not it will result in an error.
That's because an assignment like hsh[v[0]] = v[1] returns the assigned value, e.g. 'Fido'. So if you omit the last line, 'Fido' becomes the new value for hsh:
hsh = 'Fido'
hsh['cat'] = 'Whiskers'
#=> IndexError: string not matched
There's also each_with_object which works similar to inject, but assumes that you want to mutate the same object within the block. It therefore doesn't require you to return it at the end of the block: (note that the argument order is reversed)
data_hash = data_arr.each_with_object({}) do |v, hsh|
hsh[v[0]] = v[1]
end
#=> {"dog"=>"Fido", "cat"=>"Whiskers", "fish"=>"Fluffy"}
or using array decomposition:
data_hash = data_arr.each_with_object({}) do |(k, v), hsh|
hsh[k] = v
end
#=> {"dog"=>"Fido", "cat"=>"Whiskers", "fish"=>"Fluffy"}
Although to convert your array to a hash you can simply use Array#to_h, which is
[...] interpreting ary as an array of [key, value] pairs
data_arr.to_h
#=> {"dog"=>"Fido", "cat"=>"Whiskers", "fish"=>"Fluffy"}

Related

takes in a dictionary key and return an array filled with the appropriate values of the keys

Given the hash
person = {
"cats"=> 2,
"dogs"=> 1
}
I wish to construct the array
["cats", "cats", "dogs"]
"cats" appears twice because person["cats"] #=> 2. For the same reason "dogs" appears once. If the hash had a third key-value pair "pigs"=>3, I would want to return the array
["cats", "cats", "dogs", "pigs", "pigs", "pigs"]
I tried the following code.
arr = person.to_a
i = 0
new_arr = []
while i < arr.length
el = arr[i][0]
final = [new_arr << el]
print final.flatten
i += 1
end
This displays
["cats"]["cats", "dogs"] => nil
but does not seem to return a value.
new_arr
#=> ["cats", "dogs"]
As you see, I am not getting the answer I wanted and do not understand why print displays what I show above.
I would like to know what is wrong with my code and what would be a better way of doing this.
flat_map method will flatten multiple arrays into one
Array operator * creates array with multiple values
result = person.flat_map {|key, value| [key] * value}
# => ["cats", "cats", "dogs"]
Ruby has a lot of nice methods to work with collections. I believe it is better to use them instead of while loop.
You can iterate through the hash using inject
method. The first parameter in the block is the resulting array, that accumulates the result of each iteration, the second is a key/value pair.
person.inject([]) do |array, (key, value)|
array + Array.new(value, key)
end
Or it can be rewritten as a one line.
person.inject([]) { |array, (key, value)| array + Array.new(value, key) }

Iterating through hash of hashes converts elements into Array

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(:+)

Pass method to reduce/inject instead of block

What is the best way to pass a method to reduce or inject instead of block like this:
def super_process(list, item)
list ||= []
list << another_method(item) + just_another_method
end
arr = ['1', '2', '3']
arr.reduce(&method(:super_process))
I have a problem with handling of list (it's default value). It's assigned to the first element of arr on first iteration but on next iteration it's assigned to the result of the first one.
I know I can write:
arr.reduce {|list, item| list << another_method(list, item) }
But that seems quite long and inexpressive to me.
The problem in your example is due to not passing the initial value to reduce. From ruby-doc.org:
reduce { |memo, obj| block } → obj
...
If you do not explicitly specify an initial value for memo, then the
first element of collection is used as the initial value of memo.
Therefore you probably want to pass an array as the first argument. I've changed the definition of super_process to something simpler:
def super_process list, item
list.push item + 1
end
arr = [1, 2, 3]
res = arr.reduce [], &method(:super_process)
puts res
This will output
2
3
4

Return array of hashes based on hash element match

I am wondering how one would search through an array of hashes and return a value based on a search string. For example, #contacts contains the hash elements: :full_name, :city, and :email. The variable #contacts (I guess it would be an array) contains three entries (perhaps rows). Below is the code I have so far to conduct a search based on :city value. However it's not working. Can anyone give me an idea what's going on?
def search string
#contacts.map {|hash| hash[:city] == string}
end
You should use select instead of map:
def search string
#contacts.select { |hash| hash[:city] == string }
end
In your code you tried to map (or transform) your array using a block, which yields boolean values. map takes a block and invokes the block for each element of self, constructing a new array containing elements returned by the block. As the result, you got an array of booleans.
select works similar. It takes a block and iterates over the array as well, but instead of transforming the source array it returns an array containing elements for which the block returns true. So it's a selection (or filtering) method.
In order to understand the difference between these two methods it's useful to see their example definitions:
class Array
def my_map
[].tap do |result|
self.each do |item|
result << (yield item)
end
end
end
def my_select
[].tap do |result|
self.each do |item|
result << item if yield item
end
end
end
end
Example usage:
irb(main):007:0> [1,2,3].my_map { |x| x + 1 }
[2, 3, 4]
irb(main):008:0> [1,2,3].my_select { |x| x % 2 == 1 }
[1, 3]
irb(main):009:0>
You can try this:
def search string
#contacts.select{|hash| h[:city].eql?(string) }
end
This will return an array of hashes which matches string.

Ruby 'tap' method - inside assignment

Recently I discovered that tap can be used in order to "drily" assign values to new variables; for example, for creating and filling an array, like this:
array = [].tap { |ary| ary << 5 if something }
This code will push 5 into array if something is truthy; otherwise, array will remain empty.
But I don't understand why after executing this code:
array = [].tap { |ary| ary += [5] if something }
array remains empty. Can anyone help me?
In the first case array and ary point to the same object. You then mutate that object using the << method. The object that both array and ary point to is now changed.
In the second case array and ary again both point to the same array. You now reassign the ary variable, so that ary now points to a new array. Reassigning ary however has no effect on array. In ruby reassigning a variable never effects other variables, even if they pointed to the same object before the reassignment.
In other words array is still empty for the same reason that x won't be 42 in the following example:
x = 23
y = x
y = 42 # Changes y, but not x
Edit: To append one array to another in-place you can use the concat method, which should also be faster than using +=.
I want to expand on this a bit:
array = [].tap { |ary| ary << 5 if something }
What this does (assuming something is true-ish):
assigns array to [], an empty array.
array.object_id = 2152428060
passes [] to the block as ary. ary and array are pointing to the same array object.
array.object_id = 2152428060
ary.object_id = 2152428060
ary << 5 << is a mutative method, meaning it will modify the receiving object. It is similar to the idiom of appending ! to a method call, meaning "modify this in place!", like in .map vs .map! (though the bang does not hold any intrinsic meaning on its own in a method name). ary has 5 inserted, so ary = array = [5]
array.object_id = 2152428060
ary.object_id = 2152428060
We end with array being equal to [5]
In the second example:
array = [].tap{ |ary| ary += [5] if something }
same
same
ary += 5 += is short for ary = ary + 5, so it is first modification (+) and then assignment (=), in that order. It gives the appearance of modifying an object in place, but it actually does not. It creates an entirely new object.
array.object_id = 2152428060
ary.object_id = 2152322420
So we end with array as the original object, an empty array with object_id=2152428060 , and ary, an array with one item containing 5 with object_id = 2152322420. Nothing happens to ary after this. It is uninvolved with the original assignment of array, that has already happened. Tap executes the block after array has been assigned.

Resources