Ruby array of hashes, compare 2 keys and sum another key/value - ruby

In Ruby I have the following array of hashes:
[
{:qty => 1, :unit => 'oz', :type => 'mass'},
{:qty => 5, :unit => 'oz', :type => 'vol'},
{:qty => 4, :unit => 'oz', :type => 'mass'},
{:qty => 1, :unit => 'lbs', :type => 'mass'}
]
What I need to be able to do is compare the elements by the :unit and :type and then sum the :qty when they are the same. The resulting Array should look like follows:
[
{:qty => 5, :unit => 'oz', :type => 'mass'},
{:qty => 5, :unit => 'oz', :type => 'vol'},
{:qty => 1, :unit => 'lbs', :type => 'mass'}
]
If the array has multiple hashes where the :qty is nil and the :unit is empty (""), then it would only return one of those. So to extend the above example, this:
[
{:qty => 1, :unit => 'oz', :type => 'mass'},
{:qty => nil, :unit => '', :type => 'Foo'},
{:qty => 5, :unit => 'oz', :type => 'vol'},
{:qty => 4, :unit => 'oz', :type => 'mass'},
{:qty => 1, :unit => 'lbs', :type => 'mass'},
{:qty => nil, :unit => '', :type => 'Foo'}
]
would become this:
[
{:qty => 5, :unit => 'oz', :type => 'mass'},
{:qty => nil, :unit => '', :type => 'Foo'},
{:qty => 5, :unit => 'oz', :type => 'vol'},
{:qty => 1, :unit => 'lbs', :type => 'mass'}
]
EDIT: Sorry, made a mistake in the second example... it shouldn't have the o.

Start by using group_by with the keys you want, then reduce the qtys in each value into a single hash, or instead using nil if they are all nil:
properties.group_by do |property|
property.values_at :type, :unit
end.map do |(type, unit), properties|
quantities = properties.map { |p| p[:qty] }
qty = quantities.all? ? quantities.reduce(:+) : nil
{ type: type, unit: unit, qty: qty }
end
#=> [{:type=>"mass", :unit=>"oz", :qty=>5},
# {:type=>"Foo", :unit=>"", :qty=>nil},
# {:type=>"vol", :unit=>"oz", :qty=>5},
# {:type=>"mass", :unit=>"lbs", :qty=>1}]
Where properties is your second sample input data.

You're going to want enumberable.group_by
This should get you started
items.group_by { |item| item.values_at(:unit, :type) }
Output
{
["oz", "mass"]=> [
{:qty=>1, :unit=>"oz", :type=>"mass"},
{:qty=>4, :unit=>"oz", :type=>"mass"}
],
["oz", "vol"]=>[
{:qty=>5, :unit=>"oz", :type=>"vol"}
],
["lbs", "mass"]=>[
{:qty=>1, :unit=>"lbs", :type=>"mass"}
]
}

ar = [{:qty => 1, :unit => 'oz', :type => 'mass'}, {:qty => nil, :unit => '', :type => 'Foo'},
{:qty => 5, :unit => 'oz', :type => 'vol'},
{:qty => 4, :unit => 'oz', :type => 'mass'}, {:qty => 1, :unit => 'lbs', :type => 'mass'},
{:qty => nil, :unit => 'o', :type => 'Foo'}]
result = ar.each_with_object(Hash.new(0)) do |e,hsh|
if hsh.has_key?({:unit => e[:unit], :type => e[:type]})
hsh[{:unit => e[:unit], :type => e[:type]}] += e[:qty]
else
hsh[{:unit => e[:unit], :type => e[:type]}] = e[:qty]
end
end
result.map{|k,v| k[:qty] = v;k }.delete_if{|h| h[:qty].nil? and !h[:unit].empty? }
# => [{:unit=>"oz", :type=>"mass", :qty=>5},
# {:unit=>"", :type=>"Foo", :qty=>nil},
# {:unit=>"oz", :type=>"vol", :qty=>5},
# {:unit=>"lbs", :type=>"mass", :qty=>1}]
Taking #Andrew Marshall under consideration
ar = [{:qty => 1, :unit => 'oz', :type => 'mass'}, {:qty => nil, :unit => '', :type => 'Foo'},
{:qty => 5, :unit => 'oz', :type => 'vol'},
{:qty => 4, :unit => 'oz', :type => 'mass'}, {:qty => 1, :unit => 'lbs', :type => 'mass'},
{:qty => nil, :unit => 'o', :type => 'Foo'}]
result = ar.each_with_object(Hash.new(0)) do |e,hsh|
if hsh.has_key?({:unit => e[:unit], :type => e[:type]})
hsh[{:unit => e[:unit], :type => e[:type]}] += e[:qty]
else
hsh[{:unit => e[:unit], :type => e[:type]}] = e[:qty]
end
end
result.map{|k,v| k[:qty] = v;k }.delete_if{|h| h[:qty].nil? and h[:unit].empty? }
# => [{:unit=>"oz", :type=>"mass", :qty=>5},
# {:unit=>"oz", :type=>"vol", :qty=>5},
# {:unit=>"lbs", :type=>"mass", :qty=>1},
# {:unit=>"o", :type=>"Foo", :qty=>nil}]

Related

Fedex Ruby Gem: Customs Value is required - how to add?

I'm working with the Ruby gem 'FedEx', https://github.com/jazminschroeder/fedex.
I've set up my code for a development mode and I'm testing making a shipment.
However, I get stuck with the following error:
C:/Ruby22/lib/ruby/gems/2.2.0/gems/fedex-.10.1/lib/fedex/request/shipment.rb:134:in 'failure_response': Customs Value is required. (Fedex:: RateError) from C: /Ruby22/lib/ruby/gems/2.2.0/gems/fedex-.10.1/lib/fedex/request/shipment.rb:32:in 'process_request' from C: /Ruby22/lib/ruby/gems/2.2.0/gems/fedex-3.10.1/lib/fedex/shipment.rb:57:in 'ship' from C: /Ruby22/bin/css_fedex_v1.rb:92:in ''
It seems that I need to parse a 'Customs Value', probably as part of my 'packages' hash. However, I'm unable to find the relevant field for me to enter this in. Anyone who's experienced this and found a solution?
My code is as below:
require 'fedex'
fedex = Fedex::Shipment.new(:key => '***',
:password => '***',
:account_number => '***',
:meter => '***',
:mode => 'development')
shipper = { :name => "***",
:company => "***",
:phone_number => "***",
:address => "***",
:city => "***",
:postal_code => "***",
:country_code => "DK" }
recipient = { :name => "***",
:company => "***",
:phone_number => "***",
:address => "***",
:city => "***",
:postal_code => "***",
:country_code => "GB",
:residential => "false" }
packages = []
packages << {:weight => {:units => "LB", :value => 1}}
shipping_options = {:packaging_type => "YOUR_PACKAGING",
:drop_off_type => "REGULAR_PICKUP"}
rate = fedex.rate(:shipper=>shipper,
:recipient => recipient,
:packages => packages,
:shipping_options => shipping_options)
ship = fedex.ship(:shipper=>shipper,
:recipient => recipient,
:packages => packages,
:service_type => "INTERNATIONAL_PRIORITY",
:shipping_options => shipping_options)
puts ship[:completed_shipment_detail][:operational_detail][:transit_time]
Customs value is declared in their docs:
https://github.com/jazminschroeder/fedex/commit/9f1d4c67b829aaa4eeba9090c1a45d3bd507aab3#diff-4f122efb7c0d98120d8b7f0cd00998e4R106
customs_value = { :currency => "USD",
:amount => "200" }
As I understand you can pass it into the commodities hash or keep it separate.

ruby-aws Amazon Mechanical Turk

I am trying to create a HIT with a pre-created form using the ruby-aws gem and keep getting a missing params error. I have limited the missing params to the params specific to my form.
It seems my request is not being formatted correctly and there are next to no examples from Amazon. My logs say the the following params are missing:
relationship, price, environmental_consciousness, age, occasion, gender, humor, experience, local, romance, additional_information
Any help is much appreciated!
Below is my current request:
hit = mturk.createHIT(
:Operation => 'CreateHIT',
:Title => 'Find a gift based on user scores',
:Description => 'Find a gift for an individual based on survey scores.',
:MaxAssignments => 3,
:Signature => signature,
:Timestamp => timestamp,
:Reward => { :Amount => 0.25, :CurrencyCode => 'USD' },
:HITLayoutId => '3AV6FF2M2GYMGLRQEKHZ7EBN4EZOJE',
:HitLayoutParameter => {'Name' => 'additional_information', 'Value' => 'TEST'},
:HitLayoutParameter => {'Name' => 'age', 'Value' => '22'},
:HitLayoutParameter => {'Name' => 'environmental_consciousness', 'Value' => '54'},
:HitLayoutParameter => {'Name' => 'experience', 'Value' => '32'},
:HitLayoutParameter => {'Name' => 'gender', 'Value' => 'male'},
:HitLayoutParameter => {'Name' => 'humor', 'Value' => '66'},
:HitLayoutParameter => {'Name' => 'local', 'Value' => '21'},
:HitLayoutParameter => {'Name' => 'occasion', 'Value' => '43'},
:HitLayoutParameter => {'Name' => 'price', 'Value' => '33'},
:HitLayoutParameter => {'Name' => 'relationship', 'Value' => '23'},
:HitLayoutParameter => {'Name' => 'romance', 'Value' => '23'},
:Keywords => 'data collection, gifting, gifts, shopping, gift listings, presents',
:AssignmentDurationInSeconds => 300,
:LifetimeInSeconds => 604800
)
I was able to resolive the issue - AWS has terrible naming conventions. The above example does use the correct format, however HitLayoutParameter must be HITLayoutParameter - Notice the CAPITAL HIT vs Hit.
Also, when submitting multiple parameters, the should only be one HITLayoutParameter that equals an array of Name/Value pairs. Working code below.
Hope this helps someone else!
Best,
~DFO~
hit = mturk.createHIT(
:Operation => 'CreateHIT',
:Title => 'Find a gift based on user scores',
:Description => 'Find a gift for an individual based on survey scores.',
:MaxAssignments => 3,
:Signature => signature,
:Timestamp => timestamp,
:Reward => { :Amount => 0.25, :CurrencyCode => 'USD' },
:HITLayoutId => '3AV6FF2M2GYMGLRQEKHZ7EBN4EZOJE',
:HITLayoutParameter => [
{:Name => 'additional_information', :Value => 'TEST'},
{:Name => 'age', :Value => '22'},
{:Name => 'environmental_consciousness', :Value => '54'},
{:Name => 'experience', :Value => '32'},
{:Name => 'gender', :Value => 'male'},
{:Name => 'humor', :Value => '66'},
{:Name => 'local', :Value => '21'},
{:Name => 'occasion', :Value => '43'},
{:Name => 'price', :Value => '33'},
{:Name => 'relationship', :Value => '23'},
{:Name => 'romance', :Value => '23'}
],
:Keywords => 'data collection, gifting, gifts, shopping, gift listings, presents',
:AssignmentDurationInSeconds => 300,
:LifetimeInSeconds => 604800
)

How to count values in a array of hashes

I have an array of hashes
[ {:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},....
{:name => "Will", :type => "none", :product => "oranges"} ]
and was wondering if there is a simple way to count the number of product's and store the count as well as the value in an array or hash.
I want the result to be something like:
#products = [{"apples" => 2, "oranges => 1", ...}]
You can do as
array = [
{:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"}
]
array.each_with_object(Hash.new(0)) { |h1, h2| h2[h1[:product]] += 1 }
# => {"apples"=>2, "oranges"=>1}
You can use Enumerable#group_by and Enumerable#map
array.group_by{|h| h[:product]}.map{|k,v| [k, v.size]}.to_h
# => {"apples"=>2, "oranges"=>1}
While not exactly what the OP was looking for, this may be helpful to many. If you're just looking for the count of a specific product, you could do this:
array = [
{:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"}
]
array.count { |h| h[:product] == 'apples' }
# => 2
You could count:
hashes = [
{:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"}
]
hashes.inject(Hash.new(0)) { |h,o| h[o[:product]] += 1; h }
Or maybe...
hashes.instance_eval { Hash[keys.map { |k| [k,count(k)] }] }
I do not know which is the more performant, the latter seims weird to read though.
I would do:
items =[ {:name => "bob", :type => "some", :product => "apples"},
{:name => "ted", :type => "other", :product => "apples"},
{:name => "Will", :type => "none", :product => "oranges"} ]
counts = items.group_by{|x|x[:product]}.map{|x,y|[x,y.count]}
p counts #=> [["apples", 2], ["oranges", 1]]
Then if you need it as a Hash just do:
Hash[counts]

How to remove a record if it is duplicate and sum values :val

Given the following array of hashes:
list=[
{:cod => "0001", :name => "name1", :val => 10},
{:cod => "0001", :name => "name1", :val => 12},
{:cod => "0002", :name => "name2", :val => 13},
{:cod => "0002", :name => "name2", :val => 14},
{:cod => "0002", :name => "name2", :val => 14},
{:cod => "0004", :name => "name4", :val => 16},
{:cod => "0004", :name => "name4", :val => 16},
{:cod => "0004", :name => "name4", :val => 17},
{:cod => "0005", :name => "name5", :val => 17},
{:cod => "0005", :name => "name5", :val => 17},
{:cod => "0005", :name => "name5", :val => 17},
{:cod => "0006", :name => "name6", :val => 110},
{:cod => "0006", :name => "name6", :val => 10},
]
How can I remove duplicate records?
Also, how can I find the sum of the values with the key :val?
You can pass a block to the method uniq of Array to determine the uniqueness.
list.uniq { |h| h[:val] }
=> [{:cod=>"0001", :name=>"name1", :val=>10},
{:cod=>"0001", :name=>"name1", :val=>12},
{:cod=>"0002", :name=>"name2", :val=>13},
{:cod=>"0002", :name=>"name2", :val=>14},
{:cod=>"0004", :name=>"name4", :val=>16},
{:cod=>"0004", :name=>"name4", :val=>17},
{:cod=>"0006", :name=>"name6", :val=>110}]
list.map do |a|
list.select { |b| b[:cod] == a[:cod] && b[:name] == a[:name] } \
.reduce { |res, c| {:cod => c[:cod], :name => c[:name], :val => ((res[:val] + c[:val]) || c[:val])} }
end.uniq { |h| h[:cod]}.each {|c| puts c.inspect}
output:
{:name=>"name1", :cod=>"0001", :val=>22}
{:name=>"name2", :cod=>"0002", :val=>41}
{:name=>"name4", :cod=>"0004", :val=>49}
{:name=>"name5", :cod=>"0005", :val=>51}
{:name=>"name6", :cod=>"0006", :val=>120}
Use group_by:
list.group_by{|x| x[:cod]}.map{|k, v| v[0].merge({:val => v.map{|x| x[:val]}.reduce(:+)})}
Based on the answers so far, there's some confusion about what you actually mean by "remove duplicate records." My interpretation of what you mean is that you wish to only remove records that are exact duplicates. If so, then it is much simpler than the other solutions presented:
list.uniq
This returns:
[{:cod=>"0001", :name=>"name1", :val=>10},
{:cod=>"0001", :name=>"name1", :val=>12},
{:cod=>"0002", :name=>"name2", :val=>13},
{:cod=>"0002", :name=>"name2", :val=>14},
{:cod=>"0004", :name=>"name4", :val=>16},
{:cod=>"0004", :name=>"name4", :val=>17},
{:cod=>"0005", :name=>"name5", :val=>17},
{:cod=>"0006", :name=>"name6", :val=>110},
{:cod=>"0006", :name=>"name6", :val=>10}]
If you want the sum of the :val fields of the unique records, you can do this:
list.uniq.map{|h| h[:val]}.reduce(:+)
That grabs the unique elements (as above), then grabs the :val value from each, and finally applies :+ (addition) to them to get the sum.
list.uniq.group_by { |e| [e[:cod], e[:name]] }.map do |k, v|
{k => v.map { |h| h[:val] }.reduce(:+)}
end
=> [{["0001", "name1"]=>22}, {["0002", "name2"]=>27}, {["0004", "name4"]=>33}, {["0005", "name5"]=>17}, {["0006", "name6"]=>120}]

Convert string into variable within for loop

Given the following code in RUBY, I need to loop through a bunch of hashes. The problem is, varX is a string, I need it to be a variable. Any ideas?
element1_old = {:ip => "192.168.0.191", :state => "PA", :county => "ambler"}
element1_new = {:ip => "192.168.0.191", :state => "PA", :county => "warrington"}
element2_old = {:ip => "192.168.0.192", :state => "PA", :county => "ambler"}
element2_new = {:ip => "192.168.0.192", :state => "PA", :county => "ambler"}
element3_old = {:ip => "192.168.0.200", :state => "PA", :county => "warrington"}
element3_new = {:ip => "192.168.0.200", :state => "PA", :county => "ambler"}
for i in 1..3
var1 = "element#{i}_old"
var2 = "element#{i}_new"
p element"#{i}".not_in_both("element#{i}_old")
end
Throw the hashes into another hash:
h = {
'element1_old' => {:ip => "192.168.0.191", :state => "PA", :county => "ambler"},
'element1_new' => {:ip => "192.168.0.191", :state => "PA", :county => "warrington"},
'element2_old' => {:ip => "192.168.0.192", :state => "PA", :county => "ambler"},
'element2_new' => {:ip => "192.168.0.192", :state => "PA", :county => "ambler"},
'element3_old' => {:ip => "192.168.0.200", :state => "PA", :county => "warrington"},
'element3_new' => {:ip => "192.168.0.200", :state => "PA", :county => "ambler"}
}
for i in 1..3
old = h["element#{i}_old"]
new = h["element#{i}_new"]
p new.not_in_both(old)
end
I'm assuming that element"#{i}" is actually supposed to be element"#{i}"_new in your pseudo-Ruby example and that you have monkey patched not_in_both into Hash.

Resources