Ruby: inject issue when turning array into hash
Just because Ruby is dynamically and implicitly typed doesn't mean that you don't have to think about types.
The type of Enumerable#inject
without an explicit accumulator (this is usually called reduce
) is something like
reduce :: [a] → (a → a → a) → a
or in a more Rubyish notation I just made up
Enumerable[A]#inject {|A, A| A } → A
You will notice that all the types are the same. The element type of the Enumerable
, the two argument types of the block, the return type of the block and the return type of the overall method.
The type of Enumerable#inject
with an explicit accumulator (this is usually called fold
) is something like
fold :: [b] → a → (a → b → a) → a
or
Enumerable[B]#inject(A) {|A, B| A } → A
Here you see that the accumulator can have a different type than the element type of the collection.
These two rules generally get you through all Enumerable#inject
-related type problems:
- the type of the accumulator and the return type of the block must be the same
- when not passing an explicit accumulator, the type of the accumulator is the same as the element type
In this case, it is Rule #1 that bites you. When you do something like
acc[key] = value
in your block, assignments evaluate to the assigned value, not the receiver of the assignment. You'll have to replace this with
acc.tap { acc[key] = value }
See also Why Ruby inject method cannot sum up string lengths without initial value?
BTW: you can use destructuring bind to make your code much more readable:
a.inject({}) {|r, (key, value)| r[key] = value; r }
issue with using inject to convert array to hash
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"}
What is the best way to convert an array to a hash in Ruby
NOTE: For a concise and efficient solution, please see Marc-André Lafortune's answer below.
This answer was originally offered as an alternative to approaches using flatten, which were the most highly upvoted at the time of writing. I should have clarified that I didn't intend to present this example as a best practice or an efficient approach. Original answer follows.
Warning! Solutions using flatten will not preserve Array keys or values!
Building on @John Topley's popular answer, let's try:
a3 = [ ['apple', 1], ['banana', 2], [['orange','seedless'], 3] ]
h3 = Hash[*a3.flatten]
This throws an error:
ArgumentError: odd number of arguments for Hash
from (irb):10:in `[]'
from (irb):10
The constructor was expecting an Array of even length (e.g. ['k1','v1,'k2','v2']). What's worse is that a different Array which flattened to an even length would just silently give us a Hash with incorrect values.
If you want to use Array keys or values, you can use map:
h3 = Hash[a3.map {|key, value| [key, value]}]
puts "h3: #{h3.inspect}"
This preserves the Array key:
h3: {["orange", "seedless"]=>3, "apple"=>1, "banana"=>2}
Convert array of hashes to hashes without array and overwrite
You have putted an extra space in between key
and value
i.e. : sku
, I believe its a typo mistake. You can parse the data in the following manner.
array_of_hashes = [{:sku=>"1234", :name=>"Test"}, {:sku=>"5678", :name=>"Test2"}]
array_of_hashes.each do |hash|
puts "sku = #{hash[:sku]} , name = #{hash[:name]}"
# Do your stuff
end
Array to Hash Ruby
a = ["item 1", "item 2", "item 3", "item 4"]
h = Hash[*a] # => { "item 1" => "item 2", "item 3" => "item 4" }
That's it. The *
is called the splat operator.
One caveat per @Mike Lewis (in the comments): "Be very careful with this. Ruby expands splats on the stack. If you do this with a large dataset, expect to blow out your stack."
So, for most general use cases this method is great, but use a different method if you want to do the conversion on lots of data. For example, @Łukasz Niemier (also in the comments) offers this method for large data sets:
h = Hash[a.each_slice(2).to_a]
How to convert an array to a hash with specified common values
There are many ways, e.g.:
arr.zip([{}] * arr.size).to_h
or (the latter, thx @Stefan, is not probably what you wanted, since it will share one hash for all keys):
arr.product([{}]).to_h
Convert Array of objects to Hash with a field as the key
users_by_id = User.all.map { |user| [user.id, user] }.to_h
If you are using Rails, ActiveSupport provides Enumerable#index_by:
users_by_id = User.all.index_by(&:id)
Having trouble adding new elements to my hash (Ruby)
Your immediate problem is that num(word)
returns a string, and a string can't be added to a number in the line hash[char] += num(word)
. You can convert the string representation of a numeric value using .to_i
or .to_f
, as appropriate for the problem.
For the overall problem I think you've added too much complexity. The structure of the problem is:
- Create a storage object to tally up the results.
- For each string containing a stock and its associated numeric value (price? quantity?), split the string into its two tokens.
- If the first character of the stock name is one of the target values,
update the corresponding tally. This will require conversion from string to integer. - Return the final tallies.
One minor improvement is to use a Set
for the target values. That reduces the work for checking inclusion from O(number of targets) to O(1). With only two targets, the improvement is negligible, but would be useful if the list of stocks and targets increase beyond small test-case problems.
I've done some renaming to hopefully make things clearer by being more descriptive. Without further ado, here it is in Ruby:
require 'set'
def get_tallies(stocks, prefixes)
targets = Set.new(prefixes) # to speed up .include? check below
tally = Hash.new(0)
stocks.each do |line|
name, amount = line.split(/ +/) # one or more spaces is token delimiter
tally[name[0]] += amount.to_i if targets.include?(name[0]) # note conversion to int
end
tally
end
stock_list = ["ABAR 200", "CDXE 500", "BKWR 250", "BTSQ 890", "DRTY 600"]
prefixes = ["A", "B"]
p get_tallies(stock_list, prefixes)
which prints
{"A"=>200, "B"=>1140}
but that can be formatted however you like.
Related Topics
Setting Path for Whenever in Cron So It Can Find Ruby
Generating an Instagram- or Youtube-Like Unguessable String Id in Ruby/Activerecord
How to Format This International Phone Number in Rails
How to Access Attributes Using Nokogiri
How to Customize Rails Activerecord Validation Error Message to Show Attribute Value
How to Use Gems Not in a Gemfile When Working with Bundler
Why Doesn't "Rails S" Work from the App Directory
Rails Carrierwave Base64 Image Upload
Rails - Local Variables Versus Instance Variables
How to Generate Links with Trailing Slash in Rails 3
Complicated Graphviz Tree Structure
How to Convert a Ruby Object to JSON
How to Execute Custom Actions After Successful Sign in with Devise
Create Custom HTML Helpers in Ruby on Rails