如何在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的对象来生成Hash
, Hash
将其键设置为日期(数组的第二个索引),并将值设置为数组的第一个索引的总和
[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] }