如何在Ruby中对数组进行分组和求和?

我有一个像这样的数组:

ar = [[5, "2014-01-27"], [20, "2014-01-28"], [5, "2014-01-28"], [10, "2014-01-28"], [15, "2014-01-29"], [5, "2014-01-29"], [5, "2014-01-30"], [10, "2014-01-30"], [5, "2014-01-30"]] 

我最终需要做的是按日期对数组项进行分组,并总结每个子数组的第一项中的数字。

所以输出将是这样的:

 [[5, "2014-01-27"], [35, "2014-01-28"], [20, "2014-01-29"], [20, "2014-01-30"]] 

ar.group_by(&:last).map{ |x, y| [y.inject(0){ |sum, i| sum + i.first }, x] }

编辑以添加说明:
我们按最后一个值(日期)分组,产生一个哈希值:

 {"2014-01-27"=>[[5, "2014-01-27"]], "2014-01-28"=>[[20, "2014-01-28"], [5, "2014-01-28"], [10, "2014-01-28"]], "2014-01-29"=>[[15, "2014-01-29"], [5, "2014-01-29"]], "2014-01-30"=>[[5, "2014-01-30"], [10, "2014-01-30"], [5, "2014-01-30"]]} 

然后用x作为散列键映射, y作为[[number, date], [number, date]]对的数组映射。

.inject(0)表示sum开始为0 ,然后我们将每个数组的第一项(数字)添加到该总和,直到迭代所有数组并添加所有数字。

然后我们做[y, x] ,其中x是散列键(日期), y是所有数字的总和。

这种方法是有效的,因为我们使用inject来避免映射数组两次,之后不必反转值,因为我们在映射时交换了它们的位置。

编辑:有趣的是,@ bjhaid和我的答案之间的基准很接近:

  user system total real 5.117000 0.000000 5.117000 ( 5.110292) 5.632000 0.000000 5.632000 ( 5.644323) 

1000000次迭代 – 我的方法是最慢的

 h = ar.group_by(&:last) h.keys.each{|k| h[k] = h[k].map(&:first).inject(:+)} h.map(&:reverse) 
 result = {} ar.map{|v,date| result[date] ||= 0; result[date] += v} 

然后你得到一个哈希,键是日期,值是总和,你真的需要结果成为一个数组吗? 看起来你需要哈希,但我不知道上下文

也许你甚至不必在ruby中这样做,如果这一切都来自一个数据库你可以分组并与查询求和

 ar.each_with_object(Hash.new(0)) { |x,hash| hash[x[1]] += x[0] }.map(&:reverse) => [[5, "2014-01-27"], [35, "2014-01-28"], [20, "2014-01-29"], [20, "2014-01-30"]] 

解释

第一部分使用Hash.new作为提供给Enumerable#each_with_object的对象来生成HashHash将其键设置为日期(数组的第二个索引),并将值设置为数组的第一个索引的总和

 [29] pry(main)> ar.each_with_object(Hash.new(0)) { |x,hash| hash[x[1]] += x[0] } => {"2014-01-27"=>5, "2014-01-28"=>35, "2014-01-29"=>20, "2014-01-30"=>20} 

第二部分使用Enumerable #map ,它将散列中的每个key对作为一个数组生成块/ proc,在每个生成的对上调用Array#reverse以反转并生成最终数组

 [30] pry(main)> {"2014-01-27"=>5, "2014-01-28"=>35, "2014-01-29"=>20, "2014-01-30"=>20}.map(&:reverse) => [[5, "2014-01-27"], [35, "2014-01-28"], [20, "2014-01-29"], [20, "2014-01-30"]] 

我更喜欢@ sawa的解决方案,它使用group_by ,但这是另一种有助于说明这里可能的方法多样性的方法。

首先将数组转换为哈希值,将日期作为键

 h = ar.each_with_object(Hash.new {|h,k| h[k] = []}) { |(x,d),h| h[d] << x } # => {"2014-01-27"=>[5], # "2014-01-28"=>[20, 5, 10], # "2014-01-29"=>[15, 5], 

接下来,将此哈希中的每个值(数组)替换为其元素的总和:

 h.keys.each { |k| h[k] = h[k].reduce(:+) } # => ["2014-01-27", "2014-01-28", "2014-01-29", "2014-01-30"] # h => {"2014-01-27"=>5 , "2014-01-28"=>35, # "2014-01-29"=>20, "2014-01-30"=>20} 

请注意,此表达式返回键的数组,但哈希h现在具有所需的值。 因此,我们无法链接到最终声明:

 h.map(&:reverse).sort_by(&:first) # => [[ 5, "2014-01-27"], [35, "2014-01-28"], # [20, "2014-01-29"], [20, "2014-01-30"]] 

我用这种方式编写它的原因之一是鼓励你考虑使用hash g作为最终结果,而不是另一个数组。 (这也是@ sawa解决方案中前两行之后的h的值)。 考虑在代码中的后续操作中是否更有意义。

其中大部分都是直截了当的,但第each with object需要一些解释。 该object是一个哈希,由块局部变量h 。 此哈希由以下内容创建:

 Hash.new { |h,k| h[k] = [] } 

这使得默认值为空数组。 第一次穿过街区, d => "2014-01-27" 。 由于哈希最初为空,因此没有密钥"2014-01-27" 。 结果, h["2014-01-27"]被赋予默认值[] ,之后h["2014-01-27"] << 5 ,导致h => {"2014-01-27" => 5}

 ar.group_by(&:last).map{ |d,g| [g.map(&:first).inject(:+), d] }