Ruby: Inject Issue When Turning Array into Hash

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:

  1. the type of the accumulator and the return type of the block must be the same
  2. 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



Leave a reply



Submit