Is there a better way to restructure this array in Ruby? - arrays

I've got an array that looks like this:
{
year_list: [{
name: 2016,
make_list: [{
name: 'Honda',
model_list: [{
name: 'CRV',
series_list: [{
name: 'Premium Plus',
style_list: [{
name: '4D SUV',
uvc: '123abc'
}]
}]
}]
}]
}]
}
And I want to transform this array to the following, where each combination of year, make, model, series and style combined into a hash and inserted into array.
[{:year=> 2016,
:make=>"Honda",
:model=>"CRV",
:series=>"Premium Plus",
:style=>"4D SUV",
:uvc=>"123abc"}]
I have a working solution, but I'm wondering if anyone has any ideas on how to make the solution more elegant solution.
list = []
year_list.each do |y_val|
vehicle_type = {}
vehicle_type[:year] = y_val['name']
make_list = y_val['make_list']
make_list.each do |k_val|
vehicle_type[:make] = k_val['name']
model_list = k_val['model_list']
model_list.each do |m_val|
vehicle_type[:model] = m_val['name']
series_list = m_val['series_list']
series_list.each do |s_val|
vehicle_type[:series] = s_val['name']
style_list = s_val['style_list']
style_list.each do |st_val|
vehicle_type[:style] = st_val['name']
vehicle_type[:uvc] = st_val['uvc']
list << vehicle_type
end
end
end
end
end
Is there a way to utilize some of Ruby's array methods to make this a better solution?
Edit: Here's a solution that is almost working using recursion.
VEHICLES = []
ELEMENTS = [:year, :make, :model, :series, :style]
def walk_tree(arr, vehicle={})
arr[0..1].each do |a|
name = a['name']
if a.key?('uvc')
vehicle[:uvc] = a['uvc']
VEHICLES << vehicle
vehicle = {}
next
end
ELEMENTS.each do |s|
key = s.to_s + '_list'
if a.key?(key)
vehicle[s] = name
walk_tree(a[key], vehicle)
end
end
end
end
walk_tree(year_list)
pp LIST

It's possible to rewrite your solution without any additional accumulators or temporary variables which will shrink the number of code lines and make it clear. My solution uses only Array map method which will return desired result, but deeply wrapped into arrays. All external array wrappers can be removed with Array flatten method call.
Here is the output from irb console.
super_hash[:year_list].map do |year|
year[:make_list].map do |make|
make[:model_list].map do |model|
model[:series_list].map do |series|
series[:style_list].map do |style|
{
year: year[:name],
make: make[:name],
model: model[:name],
series: series[:name],
style: style[:name],
uvc: style[:uvc]
}
end
end
end
end
end
#=> [[[[[{:year=>2016, :make=>"Honda", :model=>"CRV", :series=>"Premium Plus", :style=>"4D SUV", :uvc=>"123abc"}]]]]]
[[[[[{:year=>2016, :make=>"Honda", :model=>"CRV", :series=>"Premium Plus", :style=>"4D SUV", :uvc=>"123abc"}]]]]].flatten
#=> [{:year=>2016, :make=>"Honda", :model=>"CRV", :series=>"Premium Plus", :style=>"4D SUV", :uvc=>"123abc"}]

You may recursively inject, passing all key-values as is, but looking for _list ones like this:
l = ->(h,(k,v)){
if k =~ /(\w+)_list$/
h[$1] = v.first[:name]
return v.first.inject(h, &l)
end
h[k] = v
h
}
p year_list.inject({}, &l)

Related

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 can I create instances of a class with data from a nested array?

I am creating a project where I have scraped data from a webpage for product info.
My scraping method returns a nested array with a collection of product_names, urls, and prices. I'm trying to use the nested array to create instances of my class Supplies, with attributes of a name, url, and price. The nested array has the same number of elements in each of the 3 array.
I want to ##all to return an array of instances of all products in the collection with their attributes set.
name_url_price = [[],[],[]]
class Catalog::Supplies
attr_accessor :name, :price, :url
##all = []
def initialize(name_url_price)
count = 0
while count <= name_url_price[0].length
self.name = name_url_price[0][count]
self.url = name_url_price[1][count]
self.price = name_url_price[2][count]
##all << self
count += 1
end
end
There's a lot going wrong here but nothing that can't be fixed:
Storing all the instances in a class variable is a little strange. Creating the instances and having the caller track them would be more common and less confusing.
Your while count <= ... loop isn't terminated with an end.
Your loop condition looks wrong, name_url_price[0] is the first element of name_url_price so name_url_price[0].length will, presumably, always be three. If name_url_price looks more like [ [names], [urls], [prices] ] then the condition is right but that's a strange and confusing way to store your data.
while loops for iteration are very rare in Ruby, you'd normally use name_url_place.each do ... end or something from Enumerable.
Your array indexing is possibly backwards, you want to say name_url_price[count][0], name_url_price[count][1], ... But see (3) if I'm misunderstanding how your data is structured.
Your ##all << self is simply appending the same object (self) to ##all over and over again. ##all will end up with multiple references to the same object and that object's attributes will match the last iteration of the while loop.
The initialize method is meant to initialize a single instance, having it create a bunch of instances is very strange and confusing.
It would be more common and generally understandable for your class to look like this:
class Catalog::Supplies
attr_accessor :name, :price, :url
def initialize(name, url, price)
self.name = name
self.url = url
self.price = price
end
end
And then your name_url_price array would look more like this:
name_url_price = [
[ 'name1', 'url1', 1 ],
[ 'name2', 'url2', 2 ],
[ 'name3', 'url3', 3 ],
[ 'name4', 'url4', 4 ]
]
and to get the supplies as objects, whatever wanted the list would say:
supplies = name_url_price.map { |a| Catalog::Supplies.new(*a) }
You could also use hashes in name_url_price:
name_url_price = [
{ name: 'name1', url: 'url1', price: 1 },
{ name: 'name2', url: 'url2', price: 2 },
{ name: 'name3', url: 'url3', price: 3 },
{ name: 'name4', url: 'url4', price: 4 }
]
and then create your instances like this:
supplies = name_url_price.map do |h|
Catalog::Supplies.new(
h[:name],
h[:url],
h[:price]
)
end
or like this:
supplies = name_url_price.map { |h| Catalog::Supplies.new(*h.values_at(:name, :url, :price)) }

Ruby iterating array of dictionaries

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) }

Lua - Match a string with item in array?

I am new in Lua and I want to try display an item from an array, but it's like an array inside an array.
This is my list:
local itemlist = {
{ name="blue car", price=5000 },
{ name="red car", price=10000 },
{ name="green car", price=2000 }
}
And so if I would input the text "red car" I want it to output something like this:
The red car costs 10000 dollars.
How can I do this in lua?
So far I have only found some string match examples where I can see if an array contains an item, but what I want is to output that AND the price. How do I get to the price? I have no idea where to even start.
You should read about tables and tables with sequences in the manual. Then you can decide whether to use pairs or ipairs to iterate over the table.
Another approach, if the names are going to be unique, would be the change the structure:
local itemlist = {
["blue car"] = { price=5000 },
["red car"] = { price=10000 },
["green car"] = { price=2000 }
}
-- or even
local prices = {
["blue car"] = 5000,
["red car"] = 10000,
["green car"] = 2000
}
print(itemlist["red car"].price);
print(prices["red car"]);
You don't need pattern matching in your simple example.
local str = "red car"
for _, v in ipairs(itemlist) do
if v.name == str then
print("The " .. v.name .. " costs " .. tostring(v.price) .. " dollars.")
end
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