简体   繁体   中英

Ruby sum from nested hash

How can I return the total scores, strokes and rounds from the following array?

players = [{"Angel Cabrera"=>{"score"=>2, "strokes"=>146, "rounds"=>3}},
 {"Jason Day"=>{"score"=>1, "strokes"=>145, "rounds"=>3}},
 {"Bryson DeChambeau"=>{"score"=>0, "strokes"=>144, "rounds"=>3}},
 {"Sergio Garcia"=>{"score"=>0, "strokes"=>144, "rounds"=>3}},
 {"Ian Poulter"=>{"score"=>5, "strokes"=>162, "rounds"=>3}},
 {"Vijay Singh"=>nil},
 {"Jordan Spieth"=>{"score"=>-4, "strokes"=>140, "rounds"=>3}}]

I can get the strokes by doing the following but I know that isn't the best way to do it.

  players.each do |x|
    x.values()[0]["strokes"]
  end

How can I return the sum of the strokes given the array above?

Use this code:

@total= 0
players.each do |x|
a= x.values[0]
if a.class == Hash
  @total += a["strokes"]
end
end

puts @total

Here are three ways of doing that.

Use the form of Hash#update that employs a block to determine the values of keys that are present in both hashes being merged

players.map { |g| g.first.last }.
        compact.
        each_with_object({}) { |g,h| h.update(g) { |_,o,v| o+v } }
  #=> {"score"=>4, "strokes"=>881, "rounds"=>18}

The steps:

a = players.map { |g| g.first.last }
  #=> [{"score"=> 2, "strokes"=>146, "rounds"=>3},
  #    {"score"=> 1, "strokes"=>145, "rounds"=>3},
  #    {"score"=> 0, "strokes"=>144, "rounds"=>3},
  #    {"score"=> 0, "strokes"=>144, "rounds"=>3},
  #    {"score"=> 5, "strokes"=>162, "rounds"=>3},
  #    nil,
  #    {"score"=>-4, "strokes"=>140, "rounds"=>3}] 
b = a.compact
  #=> [{"score"=> 2, "strokes"=>146, "rounds"=>3},
  #    {"score"=> 1, "strokes"=>145, "rounds"=>3},
  #    {"score"=> 0, "strokes"=>144, "rounds"=>3},
  #    {"score"=> 0, "strokes"=>144, "rounds"=>3},
  #    {"score"=> 5, "strokes"=>162, "rounds"=>3},
  #    {"score"=>-4, "strokes"=>140, "rounds"=>3}] 
b.each_with_object({}) { |g,h| h.update(g) { |_,o,v| o+v } }
  #=> {"score"=>4, "strokes"=>881, "rounds"=>18}    

Here, Hash#update (aka merge! ) uses the block { |_,o,v| o+v } { |_,o,v| o+v } ) to determine the values of keys that are present in both hashes. The first block variable (which is not used, and therefore can be represented by the local variable _ ) is the key, the second ( o , for "old") is the value of the key in h and the third ( n , for "new") is the value of the key in g .

Use a counting hash

players.map { |g| g.first.last }.
        compact.
        each_with_object(Hash.new(0)) { |g,h| g.keys.each { |k| h[k] += g[k] } }

Hash.new(0) creates an empty hash with a default value of zero, represented by the block variable g . This means that if a hash h does not have a key k , h[k] returns the default value (but does not alter the hash). h[k] += g[k] above expands to:

h[k] = h[k] + g[k]

If h does not have a key k , h[k] on the right side is therefore replaced by 0 .

Sum values and then convert to a hash

If you are using Ruby v1.9+ and the keys are guaranteed to have the same order in each hash, a third way it could be done is as follows:

["scores", "strokes", "rounds"].zip(
  players.map { |g| g.first.last }.
          compact.
          map(&:values).
          transpose.
          map { |arr| arr.reduce(:+) }
  ).to_h
  #=> {"scores"=>4, "strokes"=>881, "rounds"=>18}

The steps (starting from b above) are:

c = b.map(&:values)
  #=> [[ 2, 146, 3],
  #    [ 1, 145, 3],
  #    [ 0, 144, 3],
  #    [ 0, 144, 3],
  #    [ 5, 162, 3],
  #    [-4, 140, 3]] 
d = c.transpose
  #=> [[  2,   1,   0,   0,   5,  -4],
  #    [146, 145, 144, 144, 162, 140],
  #    [  3,   3,   3,   3,   3,   3]] 
totals = d.map { |arr| arr.reduce(:+) }
  #=> [4, 881, 18]
e = ["scores", "strokes", "rounds"].zip(totals)
  #=> [["scores", 4], ["strokes", 881], ["rounds", 18]]
e.to_h
  #=> {"scores"=>4, "strokes"=>881, "rounds"=>18} 

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