Ruby sort array of hashes by child-parent relation - ruby

So we have and array of hashes
array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
target_array = []
What is the most efficient and ruby way to map/sort that array to the following result:
target_array = [
{id:1,children:
[{id: 2, children: [
{id:4, children:[]}]}]},
{id: 3, children:[]}
]
p.s.The most I am capable of is iterating whole thing for each item and excluding from array hash that is already mapped to target_array.

You can solve this with recursion :
#array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
def build_hierarchy target_array, n
#array.select { |h| h[:parent_id] == n }.each do |h|
target_array << {id: h[:id], children: build_hierarchy([], h[:id])}
end
target_array
end
build_hierarchy [], 0
Output :
=> [{"id"=>1, "children"=>[{"id"=>2, "children"=>[{"id"=>4, "children"=>[]}]}]}, {"id"=>3, "children"=>[]}]
Live example in this ruby fiddle http://rubyfiddle.com/riddles/9b643

I would use recursion, but the following could easily be converted to a non-recursive method.
First construct a hash linking parents to their children (p2c). For this, use the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged:
#p2c = array.each_with_object({}) { |g,h|
h.update(g[:parent_id]=>[g[:id]]) { |_,ov,nv| ov+nv } }
#=> {0=>[1, 3], 1=>[2], 2=>[4]}
There are many other ways to construct this hash. Here's another:
#p2c = Hash[array.group_by { |h| h[:parent_id] }
.map { |k,v| [k, v.map { |g| g[:id] }] }]
Now construct a recursive method whose lone argument is a parent:
def family_tree(p=0)
return [{ id: p, children: [] }] unless #p2c.key?(p)
#p2c[p].each_with_object([]) { |c,a|
a << { id:c, children: family_tree(c) } }
end
We obtain:
family_tree
#=> [ { :id=>1, :children=>
# [
# { :id=>2, :children=>
# [
# { :id=>4, :children=>[] }
# ]
# }
# ]
# },
# { :id=>3, :children=>[] }
# ]
Constructing the hash #p2c initially should make it quite efficient.

This is what I tried my way using Hash
array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
target_hash = Hash.new { |h,k| h[k] = { id: nil, children: [ ] } }
array.each do |n|
id, parent_id = n.values_at(:id, :parent_id)
target_hash[id][:id] = n[:id]
target_hash[parent_id][:children].push(target_hash[id])
end
puts target_hash[0]
Output:
{:id=>nil, :children=>[{:id=>1, :children=>[{:id=>2, :children=>[{:id=>4, :children=>[]}]}]}, {:id=>3, :children=>[]}]}

I think the best one will have O(nlog(n)) time complexity at most. I'm giving my non-hash one :
array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
# This takes O(nlog(n)).
array.sort! do |a, b|
k = (b[:parent_id] <=> b[:parent_id])
k == 0 ? b[:id] <=> a[:id] : k
end
# This takes O(n)
target_array = array.map do |node|
{ id: node[:id], children: [] }
end
# This takes O(nlog(n))
target_array.each_with_index do |node, index|
parent = target_array[index + 1...target_array.size].bsearch do |target_node|
target_node[:id] == array[index][:parent_id]
end
if parent
parent[:children] << node
target_array[index] = nil
end
end
# O(n)
target_array.reverse.compact
# =>
# [{:id => 1, :children =>[{:id=>2,:children=> [ {:id=>4,
# :children=>[]}]}]},
# {:id=>3, :children=>[]} ]
So mine uses O(nlog(n)) in general.
By the way, when I simply tested out the existing solutions I found Gagan Gami's to be most efficient (slightly ahead of mine), I believe it's O(nlog(n)) too, though not obvious. But the currently accepted solution takes O(n^2) time.

Related

Convert Array of Hashes to a Hash

I'm trying to convert the following:
dep = [
{id: 1, depen: 2},
{id: 1, depen: 3},
{id: 3, depen: 4},
{id: 5, depen: 3},
{id: 3, depen: 6}
]
Into a single hash:
# {1=>2, 1=>3, 3=>4, 5=3, 3=>6}
I tried a solution I found on another question:
dep.each_with_object({}) { |g,h| h[g[:id]] = g[:dep_id] }
However, the output removed elements and gave me:
#{1=>3, 3=>6, 5=>2}
where the last element is also incorrect.
You cannot have a hash like {1=>2, 1=>3, 3=>4, 5=3, 3=>6}. All keys of a hash mst have be unique.
If you want to get a hash mapping each id to a list of dependencies, you can use:
result = dep.
group_by { |obj| obj[:id] }.
transform_values { |objs| objs.map { |obj| obj[:depen] } }
Or
result = dep.reduce({}) do |memo, val|
memo[val[:id]] ||= []
memo[val[:id]].push val[:depen]
memo
end
which produce
{1=>[2, 3], 3=>[4, 6], 5=>[3]}

Find hash in another array and substract in Ruby

I have two arrays of hashes that look like this:
a = [ { car_id: 1, motor_id: 1, quantity: 5 },
{ car_id: 1, motor_id: 2, quantity: 6 },
{ car_id: 5, motor_id: 3, quantity: 3 } ]
b = [ { car_id: 1, motor_id: 1, quantity: 2 },
{ car_id: 1, motor_id: 2, quantity: 3 } ]
I want to substract the quantities from each hash in b from the hashes in a for those hashes that have the same car_id & motor_id. So, my expected result would be:
c = [ {car_id: 1, motor_id: 1, quantity: 3},
{car_id: 1, motor_id: 2, quantity: 3},
{car_id: 5, motor_id: 3, quantity: 3 } ]
What is a good way to do this in Ruby? My thoughts were to iterate over a, and for each element find if there is any in b that have the same car_id and motor_id, if so, substract, and continue.
I suggest you first create a hash bqty for b that, for each element (hash) g of b, maps [g[:car_id], g[:motor_id]] into g[:quantity]:
bqty = b.each_with_object({}) {|g,h| h[[g[:car_id], g[:motor_id]]] = g[:quantity]}
#=> {[1, 1]=>2, [1, 2]=>3}
Next, map each element (hash) g of a to the desired hash. This is done by merging g into an empty hash h (or g.dup), then, if there is an element of bqty with the key key = [h[:car_id], h[:motor_id]], subtract bqty[key] from h[:quantity]. Note this leaves a and b unchanged.
a.map do |g|
{}.merge(g).tap do |h|
key = [h[:car_id], h[:motor_id]]
h[:quantity] -= bqty[key] if bqty.key?(key)
end
end
#=> [{:car_id=>1, :motor_id=>1, :quantity=>3},
# {:car_id=>1, :motor_id=>2, :quantity=>3},
# {:car_id=>5, :motor_id=>3, :quantity=>3}]
An alternative to the antepenultimate1 line is:
h[:quantity] -= bqty[key].to_i
since nil.to_i #=> 0.
1. How can one pass up an opportunity to use such a word?
This is basically what you suggested, except instead of an explicit check for car_id/motor_id it uses a hash instead.
ids = ->(h){h.values_at(:car_id, :motor_id)} # A function for extracting the key values
lookup = Hash[*a.flat_map{|h| [ids[h], h]}] # A map from ids -> hash w/ quantity
b.each{|h| lookup[ids[h]][:quantity] -= h[:quantity]} # Subtract each b from an a (will error if the a doesn't exist)
a == c # == true
Perhaps, you'd find the following more intuitive:
matched_result = []
while current_a = a.shift
if current_b = b.shift
if current_a[:car_id] == current_b[:car_id] && current_a[:motor_id] == current_b[:motor_id]
matched_result << {car_id: current_a[:car_id], motor_id: current_a[:motor_id], quantity: current_a[:quantity] - current_b[:quantity]}
end
else
matched_result << current_a
end
end
p matched_result

Sorting an array by category names

Given this array a:
a = [
{id: 3, category_name: "Horror"},
{id: 4, category_name: "Non-Fiction"},
{id: 5, category_name: "LGBT"},
{id: 6, category_name: "Romance"},
{id: 7, category_name: "Romance"}
]
I would like a way to order the objects in the array a by category names using the order of the array (indices), as Ruby arrays luckily are ordered arrays by default:
categories_sorted = [
"Romance",
"LGBT",
"Non-Fiction",
"Horror"
]
So that the sorting algorithm would give me an array that would look like this:
result = [
{id: 6, category_name: "Romance"},
{id: 7, category_name: "Romance"},
{id: 5, category_name: "LGBT"},
{id: 4, category_name: "Non-Fiction"},
{id: 3, category_name: "Horror"}
]
Can you point out any ways to achieve this efficiently?
The array "a" could be as big as 20 objects, and the sorted categories can have as much as 30 categories in my scenario.
do as below :-
a = [
{id: 3, category_name: "Horror"},
{id: 4, category_name: "Non-Fiction"},
{id: 5, category_name: "LGBT"},
{id: 6, category_name: "Romance"},
{id: 7, category_name: "Romance"}
]
categories_sorted = [
"Romance",
"LGBT",
"Non-Fiction",
"Horror"
]
p a.sort_by { |h| [categories_sorted.index(h[:category_name]), h[:id]] }
# >> [{:id=>6, :category_name=>"Romance"}, {:id=>7, :category_name=>"Romance"}, {:id=>5, :category_name=>"LGBT"}, {:id=>4, :category_name=>"Non-Fiction"}, {:id=>3, :category_name=>"Horror"}]
It's worth compiling a sort index list to avoid having to look things up constantly:
categories_sorted_index = Hash[
categories_sorted.each_with_index.collect do |name, index|
[ name, index ]
end
]
a.sort_by do |entry|
categories_sorted_index[entry[:category_name]] || 0
end
# =>
# {:id=>7, :category_name=>"Romance"}
# {:id=>6, :category_name=>"Romance"}
# {:id=>5, :category_name=>"LGBT"}
# {:id=>4, :category_name=>"Non-Fiction"}
# {:id=>3, :category_name=>"Horror"}
Here's a way that uses Enumerable#group_by and Hash#values_at rather than Enumerable#sort_by:
a.group_by { |h| h[:category_name] }.values_at(*categories_sorted).flatten
#=> [{:id=>6, :category_name=>"Romance"},
# {:id=>7, :category_name=>"Romance"},
# {:id=>5, :category_name=>"LGBT"},
# {:id=>4, :category_name=>"Non-Fiction"},
# {:id=>3, :category_name=>"Horror"}]

Clever Way To Get 2nd Item From Each Group In Array Of Hashes?

The problem would be easy to solve with a manual loop and a result array, which you add onto as you go. But I'm looking for a more ruby-esque solution, probably something that uses inject or select. Here's the problem:
arr_of_hashes = [
{id: 1, val: "blah1"},
{id: 1, val: "blah2"},
{id: 1, val: "blah3"},
{id: 2, val: "blah4"},
{id: 2, val: "blah5"},
{id: 3, val: "blah6"},
{id: 3, val: "blah7"},
{id: 3, val: "blah8"},
{id: 3, val: "blah9"}
]
"Groups" are defined by the "id" field in the hashes. We are guaranteed each group has at least two items. We want to return an array containing the 2nd item of each group:
output_should_be = [
{id: 1, val: "blah2"},
{id: 2, val: "blah5"},
{id: 3, val: "blah7"}
]
"I'm looking for a more ruby-esque solution". I guess you mean functional:
arr_of_hashes.chunk { |h| h[:id] }.map { |id, hs| hs[1] }
#=> [{:id=>1, :val=>"blah2"}, {:id=>2, :val=>"blah5"}, {:id=>3, :val=>"blah7"}]
Use group_by if elements are not pre-ordered by id.
Here's a solution using group_by:
arr_of_hashes.group_by{|h| h[:id]}.to_a.map{|id,a| a[1]}

Iterating over hash of arrays

I have the following:
#products = {
2 => [
#<Review id: 9, answer01: 3, score: 67, style_id: 2, consumer_id: 2,
branch_id: 2, business_id: 2>
],
15 => [
#<Review id: 10, answer01: 3, score: 67, style_id: 2, consumer_id: 2,
branch_id: 2, business_id: 2>,
#<Review id: 11, answer01: 3, score: 67, style_id: 2, consumer_id: 2,
branch_id: 2, business_id: 2>
]
}
I want to average the scores for all reviews associated with each product's hash key. How can I do this?
To iterate over a hash:
hash = {}
hash.each_pair do |key,value|
#code
end
To iterate over an array:
arr=[]
arr.each do |x|
#code
end
So iterating over a hash of arrays (let's say we're iterating over each array in each point in the hash) would be done like so:
hash = {}
hash.each_pair do |key,val|
hash[key].each do |x|
#your code, for example adding into count and total inside program scope
end
end
Yes, just use map to make and array of the scores for each product and then take the average of the array.
average_scores = {}
#products.each_pair do |key, product|
scores = product.map{ |p| p.score }
sum = scores.inject(:+) # If you are using rails, you can also use scores.sum
average = sum.to_f / scores.size
average_scores[key] = average
end
Thanks for the answer Shingetsu, I will certainly upvote it. I accidentally figured the answer out myself.
trimmed_hash = #products.sort.map{|k, v| [k, v.map{|a| a.score}]}
trimmed_hash.map{|k, v| [k, v.inject(:+).to_f/v.length]}

Resources