1

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, i.e., 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?

4 Answers 4

4
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 ;-)

Sign up to request clarification or add additional context in comments.

Comments

0

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

Comments

0

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

Comments

0

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

2 Comments

I think a small adjustment is needed: for data = [{ group: "A", result: 1 }], pass_fail_hash data #=> { :a=>{ :pass=>1, :fail=>nil } }.
@CarySwoveland thank you for pointing this out. I have resolved this now so that nil values are appropriately 0.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.