Rails sum hashes inside an array - ruby-on-rails

Basically I have an array of hashes like so :
[
{ :id => 20, :total => 1, :total2 => 0 },
{ :id => 21, :total => 1, :total2 => 0 },
{ :id => 22, :total => 2, :total2 => 0 },
{ :id => 23, :total => 1, :total2 => 0 },
{ :id => 20, :total => 1, :total2 => 0 },
{ :id => 21, :total => 1, :total2 => 0 },
{ :id => 22, :total => 1, :total2 => 1 },
{ :id => 23, :total => 1, :total2 => 0 }
]
I want the array to sum the last two hash columns like so, keeping the first (:id) as an identifier:
[
{ :id => 20, :total => 2, :total2 => 0 },
{ :id => 21, :total => 2, :total2 => 0 },
{ :id => 22, :total => 3, :total2 => 1 }
]
I have looked around and it seems that the .inject() method is used in this instance but I cannot really figure out the syntax/how to use this.
What I am looking for is to keep the first column (:id) as an ID field; if there is another hash with this ID, like in my example above, the two hashes should be added together.

Can you try this?
array = [{:stemp=>20, :vtotal=>1, :avg=>0}, {:stemp=>21, :vtotal=>1, :avg=>0},{:stemp=>22, :vtotal=>2, :avg=>0}, {:stemp=>23, :vtotal=>1, :avg=>0}, {:stemp=>20, :vtotal=>1, :avg=>0}, {:stemp=>21, :vtotal=>1, :avg=>0}, {:stemp=>22, :vtotal=>1, :avg=>1}, {:stemp=>23, :vtotal=>1, :avg=>0}]
result = array.group_by{|h| h[:stemp]}.map do |stemp, hashes|
{ stemp: stemp, vtotal: hashes.map{|h| h[:vtotal]}.inject(:+), avg: hashes.map{|h| h[:avg]}.inject(:+) }
end
Just copy-pasted it in the IRB console with ruby 1.9.3, outputs this:
[
{:stemp=>20, :vtotal=>2, :avg=>0},
{:stemp=>21, :vtotal=>2, :avg=>0},
{:stemp=>22, :vtotal=>3, :avg=>1},
{:stemp=>23, :vtotal=>2, :avg=>0}
]

I reformatted the data in the question and in my answer so that it is a little easier for others to see what is going on.
data = [
{ :stemp => 20, :vtotal => 1, :avg => 0 },
{ :stemp => 21, :vtotal => 1, :avg => 0 },
{ :stemp => 22, :vtotal => 2, :avg => 0 },
{ :stemp => 23, :vtotal => 1, :avg => 0 },
{ :stemp => 20, :vtotal => 1, :avg => 0 },
{ :stemp => 21, :vtotal => 1, :avg => 0 },
{ :stemp => 22, :vtotal => 1, :avg => 1 },
{ :stemp => 23, :vtotal => 1, :avg => 0 }
]
First, group your hashes by the stemp.
data = data.group_by { |datum| datum[:stemp] }
Then iterate over each stemp and its entries.
data = data.map do |stemp, entries|
# this pulls out each hash's :vtotal entry and then combines it with the + operator
vtotal = entries.map { |entry| entry[:vtotal] }.inject(&:+)
# this does the same as above, but for the avg entry
avg = entries.map { |entry| entry[:avg] }.inject(&:+)
{ :stemp => stemp, :vtotal => vtotal, :avg => avg }
end
This outputs
[
{ :stemp => 20, :vtotal => 2, :avg => 0 },
{ :stemp => 21, :vtotal => 2, :avg => 0 },
{ :stemp => 22, :vtotal => 3, :avg => 1 },
{ :stemp => 23, :vtotal => 2, :avg => 0 }
]

This solution suffers from readability but I wanted to provide it for reference.
Hash#merge accepts a block that will be executed when a colliding key is found.
arr = [ {:id => 20, :total => 1, :total2 => 0} ... ]
arr.group_by{ |h| h[:id] }.map do |_, hash|
hash.reduce do |hash_a, hash_b|
hash_a.merge(hash_b){ |key, old, new| key == :id ? old : old + new }
end
end

This one also works
arr.group_by{|t| t[:stemp]}.map {|key, value| value.inject({}) { |hash, values| values.merge(hash){ |key, v1, v2| key == :stemp ? v1 : v1+v2 }}}
change to :id
arr.group_by{|t| t[:id]}.map {|key, value| value.inject({}) { |hash, values| values.merge(hash){ |key, v1, v2| key == :id ? v1 : v1+v2 }}}

[{"4"=>"20.0"}, {"4"=>"20.0"}, {"4"=>"10.0"}, {"4"=>"10.0", "5"=>"10.0"}, {"4"=>"10.0", "5"=>"0.0"}, {"4"=>"10.0"}, {"4"=>"10.0"}, {"4"=>"0.0", "5"=>"10.66"}, {"4"=>"20.0"}, {"4"=>"10.0"}, {"4"=>"10.0"}, {"4"=>"0.0"}].map{|m| m.map{|k,v| v.to_f}.sum()}.sum()

Related

How to create an array of hashes by mapping 2 hashes in ruby on rails

I am using Ruby on Rails application. I want to combine 2 array of hashes with hash and to result in array of hashes.
Inputs:
first_array_of_hash = [{:name => "John", :age => 34, :mode => "nullable"},{:name => "Rose", :age => 30, :mode => "nullable"}]
second_hash = {:field_name => "", :field_age => nil, :field_nullable => false, :field_default => ""}
I want my result to be like below
result = [{:field_name => "John", :field_age => 34, :field_nullable => true, :field_default => ""},{:field_name => "Rose", :field_age => 30, :field_nullable => true, :field_default => ""}]
You can use a regular Array#map for this:
first_array_of_hash = [{:name => "John", :age => 34, :nullable => 'yes'},{:name => "Rose", :age => 30, :nullable => 'no'}]
second_hash = {:field_name => "", :field_age => nil, :field_nullable => false, :field_default => ""}
def transform(object)
{
field_name: object[:name],
field_age: object[:age],
field_nullable: object[:mode] == 'nullable'
}
end
result = first_array_of_hash.map do |object|
second_hash.merge(transform(object))
end
puts result

Include counts in as_json method

I have a Customers table and an Orders table. Each customer has multiple orders. When I do a Customers.all.as_json(:include => :orders), I get all the orders but I just want to get the count
I tried Customers.all.as_json(:include => {:orders => {:only => [], :methods => [:custom_method_to_get_order_count}}) though it gets the correct count I have array of object that are equal to count.
Wrong output! :(
[
{
:customer_id => 1,
:customer_name => "ABC",
:orders => [
{:custom_method_to_get_order_count => 2},
{:custom_method_to_get_order_count => 2}
]
},
{
:customer_id => 2,
:customer_name => "DEF",
:orders => [
{:custom_method_to_get_order_count => 3},
{:custom_method_to_get_order_count => 3},
{:custom_method_to_get_order_count => 3}
]
},
{
:customer_id => 3,
:customer_name => "XYZ",
:orders => [
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10},
{:custom_method_to_get_order_count => 10}
]
}
]
Correct output I am looking for
[
{
:customer_id => 1,
:customer_name => "ABC",
:orders => {
:custom_method_to_get_order_count => 2
}
},
{
:customer_id => 2,
:customer_name => "DEF",
:orders => {
:custom_method_to_get_order_count => 3
}
},
{
:customer_id => 3,
:customer_name => "XYZ",
:orders => {
:custom_method_to_get_order_count => 10
}
}
]
You can also use like this:
class Customers
def get_orders_count
self.orders.count
end
end
and now you can get all orders count in customer object:
Customers.all.as_json(method: :get_orders_count)
Output will be something like:
[
{
:customer_id => 1,
:customer_name => "ABC",
:get_orders_count => 12
},
{
:customer_id => 2,
:customer_name => "DEF",
:get_orders_count => 20
},
{
:customer_id => 3,
:customer_name => "XYZ",
:get_orders_count => 50
}
]
Never mind! I got the answer.
class Customers
def custom_method_to_get_order_count
Orders.where(:customer_id => id).count
end
end
What's happening to me, whenever I post the question on SO, I get the answer!

Fedex gem, multiples packages

I'm trying to calculate the shipping cost depending of the total quantity of the product purchased with the Fedex gemhttps://github.com/jazminschroeder/fedex. I'm getting the rates but I have different packages options, 3 actually.
the first one when the quantity is 1 (small), the second one when the quantity is 2 (medium) and the third one when the quantity is 3 or 4 (larger).
def packages_types
packages = []
if #order.quantity >= 4
packages << { :weight => {:units => "LB", :value => #order.case_weight},
:dimensions => {:length => 8, :width => 1, :height => 7, :units => "IN" } }
elsif #order.quantity == 2
packages << { :weight => {:units => "LB", :value => 21},
:dimensions => {:length => 1, :width => 2, :height => 7, :units => "IN" } }
elsif #order.quantity == 1
packages << { :weight => {:units => "LB", :value => 10},
:dimensions => {:length => 1, :width => 2, :height => 2, :units => "IN" } }
end
end
So if the client orders 5 on quantity. It's going to be the package of 4(large) and 1 of the small package. I was thinking using the mod...
def packages_types
packages = []
extra_items_count = #order.quantity % 4
large_packages_needed = (#order.quantity - extra_items_count) / 4
# point A
large_packages_needed.times do
packages << { :weight => { :units => "LB", :value => #order.case_weight },
:dimensions => { :length => 8, :width => 1, :height => 7, :units => "IN" } }
end
# point B
case extra_items_count
when 1
packages << { :weight => { :units => "LB", :value => 10 },
:dimensions => { :length => 1, :width => 2, :height => 2, :units => "IN" } }
when 2
packages << { :weight => { :units => "LB", :value => 21 },
:dimensions => { :length => 1, :width => 2, :height => 7, :units => "IN" } }
when 3, 4
packages << { :weight => { :units => "LB", :value => #order.case_weight },
:dimensions => { :length => 8, :width => 1, :height => 7, :units => "IN" } }
end
return packages
end
Point A: For each group of 4 items, a large package is needed (and added to the packages array).
Point B: For the remaining items (example: you ordered 5 items -> 1 large package of 4 items + 1 small for 1 remaining item), we add the corresponding conditions in the package array.

Sum mongoid specified field

def self.group_by(field, format = 'day')
key_op = [['year', '$year'], ['month', '$month'], ['day', '$dayOfMonth']]
key_op = key_op.take(1 + key_op.find_index { |key, op| format == key })
project_date_fields = Hash[*key_op.collect { |key, op| [key, {op => "$#{field}"}] }.flatten]
group_id_fields = Hash[*key_op.collect { |key, op| [key, "$#{key}"] }.flatten]
pipeline = [
{"$project" => {"name" => 1, field => 1}.merge(project_date_fields)},
{"$group" => {"_id" => group_id_fields, "count" => {"$sum" => "$QtyUsed"}}},
{"$sort" => {"count" => -1}}
]
collection.aggregate(pipeline)<br>
end
when I execute that script, count result is 0.
How can I sum attributes QtyUsed?
you are not projecting the "QtyUsed" field
pipeline = [
{"$project" => {"name" => 1, field => 1, "QtyUsed' => 1}.merge(project_date_fields)},
{"$group" => {"_id" => group_id_fields, "count" => {"$sum" => "$QtyUsed"}}},
{"$sort" => {"count" => -1}}
]

Find max value for a key of a hash of a hash

Trying to figure out a way to get the max/min values out of a hash of hash. Example would be that I want to get the player with the highest deaths and the player with the highest kills.
Example of a hash:
{ 1234 =>
{ :steamID => 1234,
:alias => "Bob",
:kills => "50",
:deaths => "10"
},
5678 =>
{ :steamID => 5678,
:alias => "Jim",
:kills => "10",
:deaths => "12"
},
}
Trying to get an output of something like:
{ :most_kills =>
{ :steamID => 12345,
:name => "Bob",
:stat => "50"},
:most_deaths =>
{ :steamID => 12345,
:name => "Bob",
:stat => "50"
}
}
The following will find the player with the max kills and deaths stats and map the output to the desired format:
input = { 1234 =>
{ steamID: 1234,
alias: "Bob",
kills: "50",
deaths: "10"
},
5678 =>
{ steamID: 5678,
alias: "Jim",
kills: "10",
deaths: "12"
},
}
output = [:kills, :deaths].each_with_object({}) do |stat, h|
max = input.max_by {|k,v| v[stat]}
h["most_#{stat}".to_sym] = {
:steamID => max[0],
:name => max[1][:alias],
:stat => max[1][stat]
}
end
The output is:
{
:most_kills => {
:steamID => 1234,
:name => "Bob",
:stat => "50"
},
:most_deaths => {
:steamID => 5678,
:name => "Jim",
:stat => "12"
}
}
I might write it like this:
def most(input, key_name)
_, v = input.max_by { |_,v| v[key_name.to_sym] }
[("most_#{key_name}").to_sym, v]
end
["deaths", "kills"].map {|key_name| most(input, key_name) }.to_h
#=> {:most_deaths=>{:steamID=>5678, :alias=>"Jim",
# :kills=>"10", :deaths=>"12"},
# :most_kills =>{:steamID=>1234, :alias=>"Bob",
# :kills=>"50", :deaths=>"10"}}

Resources