简体   繁体   中英

How to handle the case when input to reduce is hash rather than array of hashes

I have an incoming input which is sometimes an array of hashes and sometimes its just an individual hash.

hsh = {"property"=>[{"name"=>"first_name", "value"=>"Joe"}, {"name"=>"last_name", "value"=>"Doe"}]}

or

hsh = {"property"=>{"name"=>"Foo", "value"=>"Bar"}}

From this input I am trying to generate a hash with name as key and value and as value , something like this:

hsh['property'].reduce(HashWithIndifferentAccess.new) do |scan, kv_pair|
  scan.merge kv_pair['name'] => kv_pair['value']
end

Giving:

{"first_name"=>"Joe", "last_name"=>"Doe"}

This works well when hsh['property'] is an array of hashes but fails with the following error:

TypeError: no implicit conversion of String into Integer 

when it's just an hash.

How do I handle the reduce on hsh['property'] so it handles the case when input to it is hash rather than array of hashes?

It's easy to ensure something being an array. You don't need if-else. Just wrap it in a new array, then flatten that array 1 level down. I have to admit that this is not very performant, but I don't care if what I originally want to do is O(n) or higher (like mapping, reducing, sorting, whatever that requires traversing the array at least once). I won't go this way if what I originally want to do is O(log(n)) or lower (like binary search).

[hsh['property']].flatten(1).reduce(HashWithIndifferentAccess.new) do |scan, kv_pair|
  scan.merge kv_pair['name'] => kv_pair['value']
end

The first thing to fix is to get rid of merge which creates an intermediate Hash each time through the loop. That's a lot of garbage to collect. The second thing is to use the simpler each_with_index method that doesn't require chaining. This way you can just add data:

hsh['property'].each_with_object(HashWithIndifferentAccess.new) do |kv_pair, scan|
  scan[kv_pair['name']] = kv_pair['value']
end

This will work so long as property has a series of Hash objects in an array. If you have just one you'll need to special case that:

case hsh['property']
when Hash
  HashWithIndifferentAccess.new(
    hsh['property']['name'] => hsh['property']['value']
  )
when Array
  hsh['property'].each_with_object(HashWithIndifferentAccess.new) do |kv_pair, scan|
    scan[kv_pair['name']] = kv_pair['value']
  end
else
  # Uh-oh, you've got to handle this case of something random.
end

I'd convert the incoming parameter to an array of a single hash, then proceed:

def foo(aoh)
  aoh = [aoh] unless Array === aoh

  aoh # return it so we can see it's been changed

  # do stuff with the AoH
end

foo({a:1}) # => [{:a=>1}]
foo([{a:1},{b:2}]) # => [{:a=>1}, {:b=>2}]

Once it's an array of a single hash, or multiple hashes, you can iterate over the contents of the array with the same code.

If you are responsible for generating the single hash or an array of hashes, you should always generate the same type of object, an array of hashes, which makes it easier for you to deal with it. If you don't, then convert it to the same type of object and move on.

As you are using rails, there is a dedicated method for ensuring something is an array - Array#wrap . Also note that your use of #reduce is considered unindiomatic and you should use #each_with_object instead.

Here is a possible solution:

Array.wrap(hsh['property']).map { |kv| [kv['name'], kv['value']] }.to_h

If you are using ruby 2.3.0+, you can even take advantage of the cool new Hash#to_proc :

Array.wrap(hsh['property']).map { |kv| ['name', 'value'].map(&kv) }.to_h

If you insist, you can add the #with_indifferent_access at the end.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM