Ruby iterating array of dictionaries - arrays

I have a JSON data structure like this...
{
"items": [
{
"person": { // person hash }
},
{
"dog": { // dog hash }
},
{
"fruit": { // fruit hash }
},
{
“person”: { // person hash }
}
]
}
}
Each item in the array contains only one key:value pair. The key is the bot that tells me what type of item the value is.
What I'd like to do is iterate the array and run a different function for each type of item.
So I have something like this...
items = data.dig('items')
items.map do |item|
if person = item.dig('person')
transform_person(person)
elsif dog = item.dig('dog')
transform_dog(dog)
elsif fruit = item.dig('fruit')
transform_fruit(fruit)
end
end
But I feel like there should be a more elegant way to do this?
Apologies. I appear to have left some ambiguity in my question.
The initial array may contain multiple items with the same key. What I am trying to do is map to an array of items that are transformed into what is required by the front end. The input contains a strange structure and info that is not needed by the front end.
So the output array order must match the input array order.
Sorry for the confusion.

First you'll want to define the key preference in a constant:
PECKING_ORDER = %w[ person dog fruit ]
Then you can use that to find it:
def detect(item)
PECKING_ORDER.lazy.map do |key|
[ key, item.dig(key) ]
end.find do |key, v|
v
end
end
Where that can dig up the first item that's found. lazy is used here so it doesn't dig them all up needlessly, just does them one at a time until there's a hit.
This gives you a key/value pair which you can use with dynamic dispatch:
items.each do |item|
key, value = detect(item)
if (key)
send(:"transform_#{key}", value)
end
end

if you know the mapping, you could make a pseudo factory hash:
methods_mapped = {
"person" => ->(person) { do_something_with_person(person) },
"dog" => ->(dog) { do_something_with_dog(dog) },
"fruit" => ->(fruit) { do_something_with_fruit(fruit) }
}
items.map do |item|
key = item.keys.first # what if keys.size > 1 ?
method = methods_mapped.fetch(key)
method.call(item[key])
end
or you could it from the opposite direction:
methods_mapped.each do |key, method|
method.call(items.dig(key))
end

Let f be a given method that takes as an argument a hash. Without loss of generality, suppose it is as follows. This corresponds to the OP's transform_person, transform_dog and transform_fruit methods combined.
def f(h)
case h.keys.first
when :person then "somebody"
when :dog then "doggie"
when :fruit then "juicy"
end
end
Suppose we are also given (no need for dig here)
items = data[:items]
#=> [{:person=>{:name=>"Melba"}},
# {:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
# {:fruit=>{:good=>"raspberries"}}]
and
key_order = [:bird, :marsupial, :dog, :person]
We wish to find the first element k of key_order for which items contains a hash h for which h.key?(k) #=> true. If such a hash h is found we are to then execute f(h).
First compute a hash key_map.
key_map = items.each_with_object({}) { |g,h| h[g.keys.first] = g }
#=> {:person=>{:person=>{:name=>"Melba"}},
# :dog=>{:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
# :fruit=>{:fruit=>{:good=>"raspberries"}}}
Then we simply execute
k = key_order.find { |k| key_map[k] }
#=> :dog
k ? f(key_map[k]) : nil
#=> "doggie"

I would kept it simple:
items.map do |item|
do_something_with_person(item) if item.dig('person')
do_something_with_dog(item) if item.dig('dog')
do_something_with_fruit(item) if item.dig('fruit')
end
or
items.each do |item|
case item
when item.dig('person') then do_something_with_person(item)
when item.dig('dog') then do_something_with_dog(item)
when item.dig('fruit') then do_something_with_fruit(item)
end
end
or
def do_something(item)
case
when item.dig('person') then do_something_with_person(item)
when item.dig('dog') then do_something_with_dog(item)
when item.dig('fruit') then do_something_with_fruit(item)
end
end
items.map { |item| do_something(item) }

Related

Ruby on Rails - Ruby, How to add the values from two hashes with same key, without overwriting the values?

first of all thank you for helping me with my SQL question, at this point.
Now I'm struggling with another thing, makes me think if I should quit being a programmer to be honest.
Anyway, my problem is : I have an array of hashes( and inside that hash another ) like this:
[
{"A1"=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"}},
{"A1"=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}},
{"A3"=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}},
...
]
What I'm trying to accomplish is that, if there are two values with the same key, ie "A1" add those values into one whole hash, without overwriting the old values and having the month as a key desired output:
[
{"A1"=> { 1 => { :balance=> "0.0000", :price=>"9.0000"} },
{ 7 => { :balance => "34030.0000", :price => "34030.0000" } }},
and so on...
]
Is this posible?
Due the current format of the data you have, you'll need more than a couple of transformations. Most of them based on transforming the values of the resulting hash, after grouping the hashes in the array by their only key:
data
.group_by { |hash| hash.keys.first } # (1)
.transform_values { |value| value.flat_map(&:values) } # (2)
.transform_values { |values| values.index_by { |value| value[:month] } } # (3)
The first transformation is to group the current object, holding an array of hashes, by its only hash key, hence the keys.first, resulting in:
{
"A1"=>[
{"A1"=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"}},
{"A1"=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}}
],
"A3"=>[{"A3"=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}}]
}
The second, is to extract only the values from each hash, in the resulting hash, with arrays of hashes:
{
"A1"=>[
{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"},
{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}
],
"A3"=>[{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}]
}
Then, it just lacks to transform the array of hashes, to simply an hash, whose keys are the value of month:
{
"A1"=>{
1.0=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"},
7.0=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}
},
"A3"=>{4.0=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}}
}
#Sebastian's answer is excellent. For variety, let's also consider an iterative approach. Not sure if it's more efficient or easier to understand, but it's always good to understand multiple perspectives.
Setting up the input data you gave us:
arr = [
{"A1"=>{:month=>1.0, :balance=>"0.0000", :price=>"9.0000"}},
{"A1"=>{:month=>7.0, :balance=>"34030.0000", :price=>"34030.0000"}},
{"A3"=>{:month=>4.0, :balance=>"34030.0000", :price=>"34030.0000"}}]
We create a new empty hash for our results.
new_hash = {}
And now iterating over the original data. We're going to make some assumptions about the form of the data.
# We know each thing is going to be a hash.
arr.each do |hsh|
# Set up some convenient variables for keys and
# values we'll need later.
key = hsh.keys.first
value = hsh.values.first
month = value[:month]
# If the output hash doesn't yet have the key,
# give it the key and assign an empty hash to it.
new_hash[key] ||= {}
# Assign the value to the hash, keyed to the current month.
new_hash[key][month] = value
# ... and get rid of the month key that's now redundant.
new_hash[key][month].delete(:month)
end
And the result is:
{"A1"=>{1.0=>{:balance=>"0.0000", :price=>"9.0000"},
7.0=>{:balance=>"34030.0000", :price=>"34030.0000"}},
"A3"=>{4.0=>{:balance=>"34030.0000", :price=>"34030.0000"}}}
Arguably it would be more useful for the desired return value to be a hash:
h = {"A1"=>{1=>{:balance=> "0.0000", :price=> "9.0000"},
7=>{:balance=>"34030.0000", :price=>"34030.0000"}},
"A3"=>{4=>{:balance=>"34030.0000", :price=>"34030.0000"}}}
That way you could write, for example:
require 'bigdecimal'
BigDecimal(h['A1'][7][:price])
#=> 0.3403e5
See BigDecimal. BigDecimal is generally used in financial calculations because it avoids round-off errors.
This result can be obtained by changing the values of :month to integers in arr:
arr = [
{"A1"=>{:month=>1, :balance=> "0.0000", :price=> "9.0000"}},
{"A1"=>{:month=>7, :balance=>"34030.0000", :price=>"34030.0000"}},
{"A3"=>{:month=>4, :balance=>"34030.0000", :price=>"34030.0000"}}
]
and by computing:
h = arr.each_with_object({}) do |g,h|
k,v = g.flatten
(h[k] ||= {}).update(v[:month]=>v.reject { |k,_| k == :month })
end
See Hash#flatten, Hash#update (aka merge!) and Hash#reject.
One could alternatively write:
h = arr.each_with_object(Hash.new { |h,k| h[k] = {} }) do |g,h|
k,v = g.flatten
h[k].update(v[:month]=>v.reject { |k,_| k == :month })
end
See the form of Hash::new that takes a block.

How to return an array of arrays that contains hashes

This is my method that is called by the test function:
def movies_with_directors_set(source)
director = []
director1 = []
hash = {}
while outer_index < source.length do
inner_index = 0
while inner_index < source[outer_index][:movies].length do
hash[:title] = []
hash[:title] = source[outer_index][:movies][inner_index][:title]
hash[:director_name] = []
hash[:director_name] = source[outer_index][:name]
director1 << hash.dup
inner_index +=1
end
director << director1.dup
outer_index += 1
end
return director
end
This is the test code:
describe 'movies_with_directors_set' do
describe 'when given a Hash with keys :name and :movies,' do
describe 'returns an Array of Hashes that represent movies' do
describe 'and each Hash has a :director_name key set with the value that was in :name' do
# This lets "sample_data" be used in the two "it" statements below
let (:test_data) {
[
{ :name => "Byron Poodle", :movies => [
{ :title => "At the park" },
{ :title => "On the couch" },
]
},
{ :name => "Nancy Drew", :movies => [
{ :title => "Biting" },
]
}
]
}
it 'correctly "distributes" Byron Poodle as :director_name of the first film' do
# { :name => "A", :movies => [{ :title => "Test" }] }
# becomes... [[{:title => "Test", :director_name => "A"}], ...[], ... []]
results = movies_with_directors_set(test_data)
expect(results.first.first[:director_name]).to eq("Byron Poodle"),
"The first element of the AoA should have 'Byron Poodle' as :director_name"
end
it 'correctly "distributes" Nancy Drew as :director_name of the last film' do
results = movies_with_directors_set(test_data)
expect(results.last.first[:director_name]).to eq("Nancy Drew"),
"The last element of the AoA should have 'Nancy Drew' as :director_name"
end
end
end
end
end
My method returns an array of hashes but for some reason it does not want to pass the test, as it tells me the second part of the test fails.
It could be that it requires arrays within an array due to the way the test is worded.
source is the database that gets passed into the method. This specific database is shown in the test code.
If you want to transform the structure from a nested director-name/titles into director-name/title pairs, there's a much easier way of going about that. Ruby's strength is in the Enumerable library which makes data transformation really fast, efficient, and easy to express. Here's an approach worth using:
def movies_with_directors(source)
# flat_map will join together the inner arrays into a single contiguous array
source.flat_map do |set|
set[:movies].map do |movie|
{
director_name: set[:name],
title: movie[:title]
}
end
end
end
This produces a very flat, easy to navigate structure like this:
# => [{:director_name=>"Byron Poodle", :title=>"At the park"}, {:director_name=>"Byron Poodle", :title=>"On the couch"}, {:director_name=>"Nancy Drew", :title=>"Biting"}]
Where you can iterate over that and assert more easily without having to do .first.first and such.
When using Ruby always try and think in terms of data transformation, not in terms of loops. There's a multitude of tools in the Enumerable library that can perform complicated operations with a single line of code. In your case you needed a combination of flat_map and map, and you're done.
Your test data and test expectations seem to be out of sync.
You have a :name key, and then expect :director_name to be present in your test hash. Change :director_name to be :name and it should pass:
{ :name => "Nancy Drew", :movies => [
and
expect(results.last.first[:director_name]).to eq("Nancy Drew")
So your failing test is probably saying something like expected nil to be "Nancy Drew", right?

How to iterate through an array of hashes sorting the keys in Ruby

I have an array of hashes like this:
items = [{"id"=>"123", "code"=>"abc","name"=>"test", "type"=>["good"]},
{"id"=>"555", "code"=>"ddd","name"=>"foo", "type"=>["better"]},
{"id"=>"098", "code"=>"zyx","name"=>"bar", "type"=>["best"]}]
I am trying to sort each hash within the array by the key.
I tried this:
items.each { |item| item = item.sort.to_h }
It returns the same result:
[{"id"=>"123", "code"=>"abc", "name"=>"test", "type"=>["good"]},
{"id"=>"555", "code"=>"ddd", "name"=>"foo", "type"=>["better"]},
{"id"=>"098", "code"=>"zyx", "name"=>"bar", "type"=>["best"]}]
but when I try this:
items[0].sort.to_h
this is the result:
{"code"=>"abc", "id"=>"123", "name"=>"test", "type"=>["good"]}
So it looks like when I call the individual elements within items using items[x] where x is an index value within the array, it sort it.
But I need a solution to iterate through each element doing that and saving the sort.
Any thoughts?
I solved it with this:
items.map { |item| item.sort.to_h }
Thanks #SagarPandya and #Dark
If, as in the example, all the hashes have the same keys, it would be faster to perform a single sort on the keys.
sorted_keys = items.first.keys.sort
#=> ["code", "id", "name", "type"]
items.map { |item| sorted_keys.each_with_object({}) { |k,h| h[k] = item[k] } }
#=> [{"code"=>"abc", "id"=>"123", "name"=>"test", "type"=>["good"]},
# {"code"=>"ddd", "id"=>"555", "name"=>"foo", "type"=>["better"]},
# {"code"=>"zyx", "id"=>"098", "name"=>"bar", "type"=>["best"]}]
The last line could alternatively be written
items.map { |item| sorted_keys.zip(item.values_at(*sorted_keys)).to_h }

How do I look up a value in a hash based on an array of keys?

With Ruby (2.4), I want to extend the core hash functionality to search for keys based on an array and return the value from the first element that can be found from that array. I have this in my lib/core_ext/hash_with_indifferent_access.rb file ...
class CaseInsensitiveHash < HashWithIndifferentAccess
# This method shouldn't need an override, but my tests say otherwise.
def [](key)
if key.kind_of?(Array)
find_by_arr(arr)
else
super convert_key(key)
end
end
protected
def convert_key(key)
key.respond_to?(:downcase) ? key.downcase : key
end
def find_by_arr(arr)
arr.inject(self){|h, k| h.to_h[k]}
end
end
However, it is not working as expected. In my code below, the search 'h[["a", "b"]]' should produce "1", because the first element, "a", is a key in my hash.
2.4.0 :001 > h = {"a" => 1, "b" => 2}
=> {"a"=>1, "b"=>2}
2.4.0 :002 > h["a"]
=> 1
2.4.0 :003 > h[["a", "b"]]
=> nil
How do I modify my code so that I can pass in an array as a key to a hash and it will start searching for values iteratively by each element in the array?
You've almost got it, but the problem is that h = { ... } creates a plain-old Hash, not the kind you've added these methods to.
The first fix is to do this:
h = CaseInsensitiveHash["a" => 1, "b" => 2]
Then you get the right type of object and your method actually runs.
There's a typo in your [] method which can be corrected:
def [](key)
case key
when Array
find_by_arr(key)
else
super convert_key(key)
end
end
Your reference to arr doesn't work because that's not defined.
The find_by_arr method also returns the last match, not the first. This can be fixed:
def find_by_arr(arr)
self[arr.first { |key| self[key] }]
end

Find key/value pairs deep inside a hash containing an arbitrary number of nested hashes and arrays

A web service is returning a hash that contains an unknown number of nested hashes, some of which contain an array, which in turn contains an unknown number of nested hashes.
Some of the keys are not unique -- i.e. are present in more than one of the nested hashes.
However, all the keys that I actually care about are all unique.
Is there someway I can give a key to the top-level hash, and get back it's value even if the key-value pair is buried deep in this morass?
(The web service is Amazon Product Advertising API, which slightly varies the structure of the results that it gives depending on the number of results and the search types permitted in each product category.)
Here's a simple recursive solution:
def nested_hash_value(obj,key)
if obj.respond_to?(:key?) && obj.key?(key)
obj[key]
elsif obj.respond_to?(:each)
r = nil
obj.find{ |*a| r=nested_hash_value(a.last,key) }
r
end
end
h = { foo:[1,2,[3,4],{a:{bar:42}}] }
p nested_hash_value(h,:bar)
#=> 42
No need for monkey patching, just use Hashie gem: https://github.com/intridea/hashie#deepfind
user = {
name: { first: 'Bob', last: 'Boberts' },
groups: [
{ name: 'Rubyists' },
{ name: 'Open source enthusiasts' }
]
}
user.extend Hashie::Extensions::DeepFind
user.deep_find(:name) #=> { first: 'Bob', last: 'Boberts' }
For arbitrary Enumerable objects, there is another extension available, DeepLocate: https://github.com/intridea/hashie#deeplocate
Combining a few of the answers and comments above:
class Hash
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.is_a? Enumerable
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
Ruby 2.3 introduces Hash#dig, which allows you to do:
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
A variation of barelyknown's solution: This will find all the values for a key in a hash rather than the first match.
class Hash
def deep_find(key, object=self, found=[])
if object.respond_to?(:key?) && object.key?(key)
found << object[key]
end
if object.is_a? Enumerable
found << object.collect { |*a| deep_find(key, a.last) }
end
found.flatten.compact
end
end
{a: [{b: 1}, {b: 2}]}.deep_find(:b) will return [1, 2]
Despite this appearing to be a common problem, I've just spent a while trying to find/come up with exactly what I need, which I think is the same as your requirement. Neither of the links in the first response are spot-on.
class Hash
def deep_find(key)
key?(key) ? self[key] : self.values.inject(nil) {|memo, v| memo ||= v.deep_find(key) if v.respond_to?(:deep_find) }
end
end
So given:
hash = {:get_transaction_list_response => { :get_transaction_list_return => { :transaction => [ { ...
The following:
hash.deep_find(:transaction)
will find the array associated with the :transaction key.
This is not optimal as the inject will continue to iterate even if memo is populated.
I use the following code
def search_hash(hash, key)
return hash[key] if hash.assoc(key)
hash.delete_if{|key, value| value.class != Hash}
new_hash = Hash.new
hash.each_value {|values| new_hash.merge!(values)}
unless new_hash.empty?
search_hash(new_hash, key)
end
end
I ended up using this for a small trie search I wrote:
def trie_search(str, obj=self)
if str.length <= 1
obj[str]
else
str_array = str.chars
next_trie = obj[str_array.shift]
next_trie ? trie_search(str_array.join, next_trie) : nil
end
end
Note: this is just for nested hashes at the moment. Currently no array support.
Because Rails 5 ActionController::Parameters no longer inherits from Hash, I've had to modify the method and make it specific to parameters.
module ActionController
class Parameters
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.respond_to?(:each)
object = object.to_unsafe_h if object.is_a?(ActionController::Parameters)
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
end
If the key is found, it returns the value of that key, but it doesn't return an ActionController::Parameter object so Strong Parameters are not preserved.

Resources