简体   繁体   中英

Cleanest way to count multiple values in an array of hashes?

I have an array of hashes like this:

data = [
 {group: "A", result: 1},
 {group: "B", result: 1},
 {group: "A", result: 0},
 {group: "A", result: 1}, 
 {group: "B", result: 1},
 {group: "B", result: 1}, 
 {group: "B", result: 0},
 {group: "B", result: 0}
]

The group will only be either A or B, and the result will only be 1 or 0. I want to count how many times the result is 0 or 1 for each group, ie, to get a tally like so:

A: result is "1" 2 times
   result is "0" 1 time
B: result is "1" 3 times
   result is "0" 2 times

I am thinking of storing the actual results in a nested hash, like:

{ a: { pass: 2, fail: 1 }, b: { pass: 3, fail: 2 } }

but this might not be the best way, so I'm open to other ideas here.

What would be the cleanest way to do this in Ruby while iterating over the data only once? Using data.inject or data.count somehow?

stats = Hash[data.group_by{|h| [h[:group], h[:result]] }.map{|k,v| [k, v.count] }]
#=> {["A", 1]=>2, ["B", 1]=>3, ["A", 0]=>1, ["B", 0]=>2}

I'll leave the transformation to the desired format up to you ;-)

This way would go over the hash only one time:

result = Hash.new { |h, k| h[k] = { pass: 0, fail: 0 }}
data.each do |item|
  result[item[:group]][item[:result] == 0 ? :fail : :pass] += 1
end
result
# => {"A"=>{:pass=>2, :fail=>1}, "B"=>{:pass=>3, :fail=>2}}

If that is truely your desired output then something like this would work:

def pass_fail_hash(a=[],statuses=[:pass,:fail])
  a.map(&:dup).group_by{|h| h.shift.pop.downcase.to_sym}.each_with_object({}) do |(k,v),obj|
    obj[k] = Hash[statuses.zip(v.group_by{|v| v[:result]}.map{|k,v| v.count})]
    statuses.each {|status| obj[k][status] ||= 0 }
  end
end

Then

pass_fail_hash data
#=>  {:a=>{:pass=>2, :fail=>1}, :b=>{:pass=>3, :fail=>2}}

Thank you to @CarySwoveland for pointing out my original method did not take into account cases where there were no passing or failing values. This has now been resolved so that a hash array like [{ group: "A", result: 1 }] will now show {a:{:pass => 1, :fail => 0}} where it would have previously been {a:{:pass => 1, :fail => nil}} .

You could use the form of Hash#update (same as Hash#merge! ) that takes a block to determine the values of keys that are contained in both hashes being merged:

data.map(&:values).each_with_object({}) { |(g,r),h|
  h.update({g.to_sym=>{pass: r, fail: 1-r } }) { |_,oh,nh|
   { pass: oh[:pass]+nh[:pass], fail: oh[:fail]+nh[:fail] } } }
  #=> {:A=>{:pass=>2, :fail=>1}, :B=>{:pass=>3, :fail=>2}}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM