Array with hash, how to merge same keys and add its value - ruby

I have an array with hashes in it. If they have the same key I just want to add its value.
#receivers << result
#receivers
=> [{:email=>"user_02#yorlook.com", :amount=>10.00}]
result
=> {:email=>"user_02#yorlook.com", :amount=>7.00}
I want the result of above to look like this
[{:email=>"user_02#yorlook.com", :amount=>17.00}]
Does anyone know how to do this?
Here is the the entire method
def receivers
#receivers = []
orders.each do |order|
product_email = order.product.user.paypal_email
outfit_email = order.outfit_user.paypal_email
if order.user_owns_outfit?
result = { email: product_email, amount: amount(order.total_price) }
else
result = { email: product_email, amount: amount(order.total_price, 0.9),
email: outfit_email, amount: amount(order.total_price, 0.1) }
end
#receivers << result
end
end

Using Enumerable#group_by
#receivers.group_by {|h| h[:email]}.map do |k, v|
{email: k, amount: v.inject(0){|s,h| s + h[:amount] } }
end
# => [{:email=>"user_02#yorlook.com", :amount=>17.0}]
Using Enumerable#each_with_object
#receivers.each_with_object(Hash.new(0)) {|h, nh| nh[h[:email]]+= h[:amount] }.map do |k, v|
{email: k, amount: v}
end

# Output: [{ "em#il.one" => 29.0 }, { "em#il.two" => 39.0 }]
def receivers
return #receivers if #receivers
# Produces: { "em#il.one" => 29.0, "em#il.two" => 39.0 }
partial_result = orders.reduce Hash.new(0.00) do |result, order|
product_email = order.product.user.paypal_email
outfit_email = order.outfit_user.paypal_email
if order.user_owns_outfit?
result[product_email] += amount(order.total_price)
else
result[product_email] += amount(order.total_price, .9)
result[outfit_email] += amount(order.total_price, .1)
end
result
end
#receivers = partial_result.reduce [] do |result, (email, amount)|
result << { email => amount }
end
end

I would just write the code this way:
def add(destination, source)
if destination.nil?
return nil
end
if source.class == Hash
source = [source]
end
for item in source
target = destination.find {|d| d[:email] == item[:email]}
if target.nil?
destination << item
else
target[:amount] += item[:amount]
end
end
destination
end
usage:
#receivers = []
add(#receivers, {:email=>"user_02#yorlook.com", :amount=>10.00})
=> [{:email=>"user_02#yorlook.com", :amount=>10.0}]
add(#receivers, #receivers)
=> [{:email=>"user_02#yorlook.com", :amount=>20.0}]

a = [
{:email=>"user_02#yorlook.com", :amount=>10.0},
{:email=>"user_02#yorlook.com", :amount=>7.0}
]
a.group_by { |v| v.delete :email } # group by emails
.map { |k, v| [k, v.inject(0) { |memo, a| memo + a[:amount] } ] } # sum amounts
.map { |e| %i|email amount|.zip e } # zip to keys
.map &:to_h # convert nested arrays to hashes

From what I understand, you could get away with just .inject:
a = [{:email=>"user_02#yorlook.com", :amount=>10.00}]
b = {:email=>"user_02#yorlook.com", :amount=>7.00}
c = {email: 'user_03#yorlook.com', amount: 10}
[a, b, c].flatten.inject({}) do |a, e|
a[e[:email]] ||= 0
a[e[:email]] += e[:amount]
a
end
=> {
"user_02#yorlook.com" => 17.0,
"user_03#yorlook.com" => 10
}

Related

Ruby Trees with full path on leaves

I have an array of paths:
paths = ["home", "usr/lib/folder1/", "usr/lib/folder2/"]
I tried to make a tree with full path on the ends (leaves):
{"home" => "home", "usr" => {"lib" => {"folder1" => "usr/lib/folder1/", "folder2" => "usr/lib/folder2/"}}}
This is my code:
paths.each do |path|
current = tree
path.split('/').inject('') do |_sub_path, dir|
sub_path = File.join(dir)
current[sub_path] ||= {path => {}}
current = current[sub_path]
sub_path
end
end
Can you show me the right way for my question?
This is my first question on SO. Sorry if my English bad.
This will do it but its not tested nor is it very Rubyish...
paths = ["home", "usr/lib/folder1/", "usr/lib/folder2/"]
ans =
paths.reduce({}){
|acc, e|
last = e.split('/').last
e.split('/').reduce(acc){
|acc, n|
acc[n] = {} if acc[n].nil?
acc[n] = e if n == last
acc[n]
}
acc
}
p ans
What's your strategy for adding more paths?I.E. If you have to add another path like "home/user/data" and "/home/user/docs" or "usr/lib/folder1/data" and "usr/lib/folder2/data", what's your strategy?
This could be solved recursively.
def recurse(arr, arr_build = [])
return arr_build.join('/') if arr == [[]]
arr.group_by { |a| a.first }.
transform_values { |v| recurse(v.map { |a| a.drop(1) },
arr_build + [v.first.first]) }
end
recurse(paths.map { |s| s.split('/') })
#=> {"home"=>"home",
# "usr"=>{"lib"=>{"folder1"=>"usr/lib/folder1",
# "folder2"=>"usr/lib/folder2"}}}
See Enumerable#group_by and Hash#transform_values.
Note that when
arr = paths.map { |s| s.split('/') }
#=> [["home"], ["usr", "lib", "folder1"], ["usr", "lib", "folder2"]]
we find that
arr.group_by { |a| a.first }
#=> {"home"=>[["home"]],
# "usr"=>[["usr", "lib", "folder1"], ["usr", "lib", "folder2"]]}

Construct hash dynamically in Ruby

I have the following ruby code which creates a hash with specified format:
result.each do |result|
if domain == 'social'
hash[result['date']] = {
'positive' => result['positive'],
'negative' => result['negative'],
'declined' => result['declined']
}
end
if domain == 'motivation'
hash[result['date']] = {
'high' => result['high'],
'medium' => result['medium'],
'low' => result['low']
}
end
end
Is there any way to remove this duplications and do this in more clean way?
Maybe creating the hash for hash[result['date']] depending on the domain value?:
result.each do |result|
keys = case domain
when 'social' then %w[positive negative declined]
when 'motivation' then %w[high medium low]
end
hash[result['date']] = keys.each_with_object(Hash.new(0)) { |e, h| h[e] = result[e] }
end
Or:
result.each do |result|
keys = domain == 'social' ? %w[positive negative declined] : %w[high medium low]
hash[result['date']] = keys.each_with_object(Hash.new(0)) { |e, h| h[e] = result[e] }
end
You can use Hash#select:
social_keys = ['positive', 'negative', 'declined']
hash[result['date']] = result.select {|k, _| social_keys.include? k }
result.each do |result|
hash[result['date']] = result.slice(
*case domain
when "social" then %w[positive negative declined]
when "motivation" then %w[high medium low]
end
)
end

Difference between 2 different nested hash in Ruby 1.8.7

Consider the Following nested Hash:
data1 = {
"3"=>{"passenger_type"=>"ADT", "the_order"=>"3", "last"=>"JONES", "first"=>"ALENA", "middle"=>nil},
"2"=>{"passenger_type"=>"ADT", "the_order"=>"2", "last"=>"JONES", "first"=>"MAXIM", "middle"=>nil},
"1"=>{"passenger_type"=>"ADTT", "the_order"=>"1", "last"=>"JONES", "first"=>"TODD", "middle"=>nil}}
data2 = {
"3"=>{"first"=>"ALENA", "the_order"=>"3", "middle"=>"", "passenger_type"=>"ADTT", "last"=>"JONES"},
"2"=>{"first"=>"MAXIM", "the_order"=>"2", "middle"=>"", "passenger_type"=>"ADT", "last"=>"JONES"},
"1"=>{"first"=>"TODD", "the_order"=>"1", "middle"=>"", "passenger_type"=>"ADT", "last"=>"JONESS"}}
The Output Should be like this(difference between both hash listed values):
{"3" => {"passenger_type" => ["ADT", "ADTT"]},
"1" => {"passenger_type" => ["ADTT", "ADT"], "last" => ["JONES", "JONESS"]}
Anyone your suggestion is appreciated, thanks in advance.
You can use the form of Hash#merge that takes a block to produce the desired result in a compact manner:
data1.merge(data2) { |_,ho,hn|
ho.merge(hn) { |_,o,n| (o==n||o==''||n=='') ? nil : [o,n] }
.delete_if { |_,v| v==nil } }
.delete_if { |_,v| v.empty? }
#=> {"3"=>{"passenger_type"=>["ADT", "ADTT"]},
# "1"=>{"passenger_type"=>["ADTT", "ADT"], "last"=>["JONES", "JONESS"]}}
Here's some ugly code:
data3 = {}
data1.each do |k, v|
v2 = data2[k]
v.each do |item, val|
if v2.has_key?(item) then
if (val == nil or val == '') and (v2[item] == nil or v2[item] == '') then
next
end
if val != v2[item] then
data3[k] ||= {}
data3[k][item] = [val, v2[item]]
end
end
end
end
puts data3
prints
{"3"=>{"passenger_type"=>["ADT", "ADTT"]}, "1"=>{"passenger_type"=>["ADTT", "ADT"], "last"=>["JONES", "JONESS"]}}

Building Hash from Array of Hashes

I'm new to Ruby and trying to solve a problem. I have an array of hashes:
list = [{"amount"=>2.25,"rel_id"=>1103, "date"=>"2012-12-21"},
{"amount"=>2.75,"rel_id"=>1103, "date"=>"2012-12-24"},
{"amount"=>2.85,"rel_id"=>666, "date"=>"2012-12-27"},
{"amount"=>3.15,"rel_id"=>666, "date"=>"2012-12-28"}
#and many many more..
]
I need to group them by rel_id, that i could see total amount and dates they were given, in this kind of format:
{1103=>{:total_amount=>5.0, :dates=>["2012-12-21", "2012-12-24"]}, 666=>{:total_amount=>6.0, :dates=>["2012-12-27", "2012-12-28"]}}
I solved this in this way, but i'm pretty sure it's one of the worst approach to do that and i think it's not a ruby way..
results = {}
list.each do |line|
if !(results.has_key?(line["rel_id"]))
results[line["rel_id"]]={:total_amount=>line["amount"],:dates=>[line["date"]]}
else
results[line["rel_id"]][:total_amount] = results[line["rel_id"]][:total_amount]+line["amount"]
results[line["rel_id"]][:dates]<<line["date"]
end
end
Maybe you could give me or explain how to implement a nicer, more beautiful approach in a ruby way?
You can do something like this:
list.each_with_object({}) do |details, rollup|
rollup[details["rel_id"]] ||= { total_amount: 0, dates: [] }
rollup[details["rel_id"]][:total_amount] += details["amount"]
rollup[details["rel_id"]][:dates] << details["date"]
end
Edited for readability/names.
Functional approach (I'll use mash, use Hash[...] if no Facets):
purchases_grouped = list.group_by { |p| p["rel_id"] }
result = purchases_grouped.mash do |rel_id, purchases|
total_amount = purchases.map { |p| p["amount"] }.reduce(:+)
dates = purchases.map { |p| p["date"] }
accumulated = {total_amount: total_amount, dates: dates}
[rel_id, accumulated]
end
#=> {1103=>{:total_amount=>5.0, :dates=>["2012-12-21", "2012-12-24"]},
# 666 =>{:total_amount=>6.0, :dates=>["2012-12-27", "2012-12-28"]}}
h = list.group_by{|h| h["rel_id"]}
h.each{|k, v| h[k] = {
total_amount: v.inject(0){|x, h| x + h["amount"]},
dates: v.map{|h| h["date"]},
}}
h # => ...
Or
h = list.group_by{|h| h["rel_id"]}
h.each{|k, v| h[k] = {
total_amount: v.map{|h| h["amount"]}.inject(:+),
dates: v.map{|h| h["date"]},
}}
h # => ...
list = [
{amount: 2.25, rel_id: 1103, date: "2012-12-21"},
{amount: 2.75, rel_id: 1103, date: "2012-12-24"},
{amount: 2.85, rel_id: 666, date: "2012-12-27"},
{amount: 3.15, rel_id: 666, date: "2012-12-28"},
]
results = Hash.new do |hash, key|
hash[key] = {}
end
list.each do |hash|
totals = results[hash[:rel_id]]
totals[:amount] ||= 0
totals[:amount] += hash[:amount]
totals[:dates] ||= []
totals[:dates] << hash[:date]
end
p results
--output:--
{1103=>{:amount=>5.0, :dates=>["2012-12-21", "2012-12-24"]},
666=>{:amount=>6.0, :dates=>["2012-12-27", "2012-12-28"]}}
Alex Peachey's each_with_object solution modified:
results = list.each_with_object({}) do |h, acc|
record = acc[h["rel_id"]]
record ||= { total_amount: 0, dates: [] }
record[:total_amount] += h["amount"]
record[:dates] << h["date"]
end

How to convert deep hash to array of keys

I want to programmatically convert this:
{
"a"=>
{"1"=>
{"A"=>
{"Standard"=>"true"}
}
},
"b"=>
{"1"=>
{"A"=>
{"Standard"=>"true"}
}
}
}
to an array like this:
['a/1/A/Standard', 'b/1/A/Standard']
def extract_keys(hash)
return [] unless hash.is_a?(Hash)
hash.each_pair.map {|key, value| [key, extract_keys(value)].join('/') }
end
extract_keys(hash)
=> ["a/1/A/Standard", "b/1/A/Standard"]
From one of my other answers - adapted for your situation. See the link for a more verbose solution to flat_hash
def flat_hash(hash, k = "")
return {k => hash} unless hash.is_a?(Hash)
hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + '/' + v[0]) }
end
example = {...} # your example hash
foo = flat_hash(example).keys
=> ["/a/1/A/Standard", "/b/1/A/Standard"]
Found this flatten lambda definition.
h = {
"a"=>
{"1"=>
{"A"=>
{"Standard"=>"true"}
}
},
"b"=>
{"1"=>
{"A"=>
{"Standard"=>"true"}
}
}
}
a = []
flatten =
lambda {|r|
(recurse = lambda {|v|
if v.is_a?(Hash)
v.to_a.map{|v| recurse.call(v)}.flatten
elsif v.is_a?(Array)
v.flatten.map{|v| recurse.call(v)}
else
v.to_s
end
}).call(r)
}
h.each do |k,v|
a << k + "/" + flatten.call(v).join("/")
end
Output:
["a/1/A/Standard/true", "b/1/A/Standard/true"]

Resources