How to count values in a array of hashes - ruby

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]

Related

XML nodes getting deleted when converting nested hash to XML in Ruby

I use the following code to convert hashes into XML:
class Hash
def to_xml
map do |k,v|
text = Hash === v ? v.to_xml : v
"<%s>%s</%s>" % [k,text,k]
end.join
end
def to_xml_with_namespace(ns)
map do |k,v|
text = Hash === v ? v.to_xml_with_namespace(ns) : v
"<#{ns}:%s>%s</#{ns}:%s>" % [k,text,k]
end.join
end
end
Everything works fine until I have multiple nodes with the same values, example:
{:users => {
:name_age_node => {:name => "Bob", :age => 50},
:name_age_node => {:name => "Tom", :age => 45},
:name_age_node => {:name => "Jess", :age => 22}
}
What outputs is simply the last node only.. The other nodes get overwritten for some reason. All nodes where there are not duplicate nodes with the same name are fine, whether they are nested or not.
Any ideas on why this could be happening?
You're dealing with hashes. A hash only supports a single occurrence of a particular key:
foo = {:users => {
:name_age_node => {:name => "Bob", :age => 50},
:name_age_node => {:name => "Tom", :age => 45},
:name_age_node => {:name => "Jess", :age => 22}
}
}
foo
# => {:users=>{:name_age_node=>{:name=>"Jess", :age=>22}}}
With unique keys:
foo = {:users => {
:name_age_node1 => {:name => "Bob", :age => 50},
:name_age_node2 => {:name => "Tom", :age => 45},
:name_age_node3 => {:name => "Jess", :age => 22}
}
}
foo
# => {:users=>
# {:name_age_node1=>{:name=>"Bob", :age=>50},
# :name_age_node2=>{:name=>"Tom", :age=>45},
# :name_age_node3=>{:name=>"Jess", :age=>22}}}
Or you could use an array of hashes to contain the inner data:
foo = {:users => [
{:name => "Bob", :age => 50},
{:name => "Tom", :age => 45},
{:name => "Jess", :age => 22}
]
}
foo
# => {:users=>
# [{:name=>"Bob", :age=>50},
# {:name=>"Tom", :age=>45},
# {:name=>"Jess", :age=>22}]}
# :name_age_node3=>{:name=>"Jess", :age=>22}}}

Nested hash iteration: How to iterate a merge over an ( (array of hashes) within a hash )

I'm trying to do as the title says. Here is my code:
school.each { |x| school[:students][x].merge!(semester:"Summer") }
I think I pinpointed the problem to the "[x]" above. If I substitute an array position such as "[2]" it works fine. How can make the iteration work?
If the info above is not enough or you'd like to offer a better solution, please see the details below. Thanks!
The error message I get:
file.rb:31:in []': no implicit conversion of Array into Integer (TypeError)
from file.rb:31:inblock in '
from file.rb:31:in each'
from file.rb:31:in'
The nested hash below before alteration:
school = {
:name => "Happy Funtime School",
:location => "NYC",
:instructors => [
{:name=>"Blake", :subject=>"being awesome" },
{:name=>"Ashley", :subject=>"being better than blake"},
{:name=>"Jeff", :subject=>"karaoke"}
],
:students => [
{:name => "Marissa", :grade => "B"},
{:name=>"Billy", :grade => "F"},
{:name => "Frank", :grade => "A"},
{:name => "Sophie", :grade => "C"}
]
}
I'm trying to append :semester=>"Summer" to each of the last four hashes. Here is what I'm trying to go for:
# ...preceding code is the same. Changed code below...
:students => [
{:name => "Marissa", :grade => "B", :semester => "Summer"},
{:name=>"Billy", :grade => "F", :semester => "Summer"},
{:name => "Frank", :grade => "A", :semester => "Summer"},
{:name => "Sophie", :grade => "C", :semester => "Summer"}
]
}
Just iterate over the students:
school[:students].each { |student| student[:semester] = "Summer" }
Or, using merge:
school[:students].each { |student| student.merge!(semester: "Summer") }
The issue is that when you do array.each {|x| do something}, x actually refers to each element in the array.
For example, in the first iteration of the loop,
x = {:name => "Marissa", :grade => "B"}
So what you are really doing is trying to reference:
school[:student][{:name => "Marissa", :grade => "B"}]
Which will not work
What you could do instead is create a for loop to track the index.
for i in 0 ... school[:student].count
school[:students][i].merge!(semester:"Summer")
end
Edit: Stefan's solution is much better than mine, but I will leave this up to show where you went wrong.
I would do as below using Hash#store :
require 'awesome_print'
school = {
:name => "Happy Funtime School",
:location => "NYC",
:instructors => [
{
:name => "Blake",
:subject => "being awesome"
},
{
:name => "Ashley",
:subject => "being better than blake"
},
{
:name => "Jeff",
:subject => "karaoke"
}
],
:students => [
{
:name => "Marissa",
:grade => "B"
},
{
:name => "Billy",
:grade => "F"
},
{
:name => "Frank",
:grade => "A"
},
{
:name => "Sophie",
:grade => "C"
}
]
}
school[:students].each{|h| h.store(:semester ,"Summer")}
ap school,:index => false,:indent => 10
output
{
:name => "Happy Funtime School",
:location => "NYC",
:instructors => [
{
:name => "Blake",
:subject => "being awesome"
},
{
:name => "Ashley",
:subject => "being better than blake"
},
{
:name => "Jeff",
:subject => "karaoke"
}
],
:students => [
{
:name => "Marissa",
:grade => "B",
:semester => "Summer"
},
{
:name => "Billy",
:grade => "F",
:semester => "Summer"
},
{
:name => "Frank",
:grade => "A",
:semester => "Summer"
},
{
:name => "Sophie",
:grade => "C",
:semester => "Summer"
}
]
}

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

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

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

ruby language - merge an array into another by finding same element

A = [
{ :id => 1, :name => 'good', :link => nil },
{ :id => 2, :name => 'bad', :link => nil }
]
B = [
{ :id => 3, :name => 'good' },
{ :id => 4, :name => 'good' },
{ :id => 5, :name => 'bad' }
]
I need to merge array B into A so that :link in array A includes the entry in array B if :name is the same value in each array.
For example, after processing array A should be:
A = [
{ :id => 1, :name => 'good', :link => [{ :id => 3, :name => 'good' }, { :id => 4, :name => 'good' }] },
{ :id => 2, :name => 'bad', :link => [{ :id => 5, :name => 'bad' }] }
]
thanks.
The short version;
a.each { | item | item[:link] = b.find_all { | x | x[:name] == item[:name] } }
Demo here.
In ruby the constants begin with an uppercase letter, so you should use lowercase letter:
A => a, B => b
a.each do |ha|
b.each do |hb|
if ha[:name] == hb[:name]
ha[:link] |= []
ha[:link] << hb
end
end
end
Functional approach:
B_grouped = B.group_by { |h| h[:name] }
A2 = A.map { |h| h.merge(:link => B_grouped[h[:name]]) }
#=> [{:link=>[{:name=>"good", :id=>3}, {:name=>"good", :id=>4}], :name=>"good", :id=>1},
# {:link=>[{:name=>"bad", :id=>5}], :name=>"bad", :id=>2}]

Resources