Ruby:选择具有多个条件的分组数组

我有一系列交易。 我需要按名称对事务进行分组,然后选择具有最高数量和多于1个实例的组。

例如,如果我有1个名为“car”的交易,金额为3000美元,3个交易“船”总计1800美元,4个交易“房子”总计500美元,该方法将选择船,因为它是最高金额组有多个交易。

@transactions = [{"amount"=>-3000, "name"=>"CAR"}, {"amount"=>-600, "name"=>"BOAT"}, {"amount"=>-600, "name"=>"BOAT"}, {"amount"=>-600, "name"=>"BOAT"}, {"amount"=>-125, "name"=>"HOUSE" }, {"amount"=>-125, "name"=>"HOUSE" }, {"amount"=>-125, "name"=>"HOUSE" }, {"amount"=>-125, "name"=>"HOUSE" }] 

现在我有这个,但它根据名称的长度选择。

 @transactions.group_by {|h| h['name'] }.max_by {|k, v| v.length }.first 

如何分组,然后求和,然后在具有多个交易的组中选择最高金额。

这里有很多好的答案。 我想补充一点,你可以通过组合操作来消除大量的迭代。

例如,您可以在group_by块中执行此操作,而不是在第二步中计算每个组的总和:

 sums = Hash.new(0) groups = transactions.group_by do |t| sums[t["name"]] += t["amount"] t["name"] end p groups # => { "CAR" => [ { "amount" => -3000, "name" => "CAR" } ], # "BOAT" => [ ... ], # "HOUSE" => [ ... ] } p sums # => { "CAR" => -3000, "BOAT" => -1800, "HOUSE" => -500 } 

接下来而不是使用groups.select来消除只有一个成员的组然后min_by来获得最终结果,将前者合并到后者中:

 result = groups.min_by do |k,g| g.size > 1 ? sums[k] : Float::INFINITY end p result[1] # => [ { "amount" => -600, "name" => "BOAT" }, # { "amount" => -600, "name" => "BOAT" }, # { "amount" => -600, "name" => "BOAT" } ] 

因为一切都小于Float::INFINITY ,所以永远不会选择那些只有一个成员的组(除非每个组只有一个成员)。

所以…

解决方案1

把它们放在一起:

 sums = Hash.new(0) result = transactions.group_by {|t| sums[t["name"]] += t["amount"] t["name"] }.min_by {|k,g| g.size > 1 ? sums[k] : Float::INFINITY }[1] p result # => [ { "amount" => -600, "name" => "BOAT" }, # { "amount" => -600, "name" => "BOAT" }, # { "amount" => -600, "name" => "BOAT" } ] 

解决方案2

您还可以将所有这些组合成一个reduce并仅对数据进行一次迭代,但它不是非常Rubyish:

 sums = Hash.new(0) groups = Hash.new {|h,k| h[k] = [] } min_sum = Float::INFINITY result = transactions.reduce do |min_group, t| name = t["name"] sum = sums[name] += t["amount"] (group = groups[name]) << t if group.size > 1 && sum < min_sum min_sum, min_group = sum, group end min_group end 

请注意,您可以将变量声明之外的所有内容移动到,例如,传递给reduce (而不是nil )的数组,但它会大大影响可读性。

只是出于好奇:

 [*transactions.each_with_object( Hash.new { |h, k| h[k] = {count: 0, total: 0} } ) do |h, memo| memo[h['name']].tap do |ct| ct[:count] += 1 ct[:total] -= h['amount'] end end.reject { |_, v| v[:count] == 1 } .sort_by { |_, v| v[:total] }].to_h #⇒ { # "BOAT" => { # :count => 3, # :total => 1800 # }, # "HOUSE" => { # :count => 4, # :total => 500 # } # } 

现在可以first调用结果,或使用max_by而不是sort_by来仅检索一个max元素。

 @transactions.group_by { |h| h['name'] } .map { |k, v| [k, v.inject(0) { |acc, cur| acc + cur['amount'] }] } .max_by(&:last).first 

当你想要Enumerable的单个值时,你可能需要reduceinject


有关详细信息,请参阅Enumerable#inject文档。

这是一种创建散列数组而不是从给定数组中选择散列的方法。

 def doit(transactions) name, arr = transactions.each_with_object(Hash.new { |h,k| h[k]=[] }) { |g,h| h[g["name"]] << g["amount"] }. reject { |_,v| v.size == 1 }. min_by { |_,v| v.reduce(:+) } name ? arr.map { |v| { "amount"=>v, "name"=>name } } : [] end 

例子

 doit(@transactions) #=> [{"amount"=>-600, "name"=>"BOAT"}, # {"amount"=>-600, "name"=>"BOAT"}, # {"amount"=>-600, "name"=>"BOAT"}] doit([{"amount"=>-3000, "name"=>"CAR"}, {"amount"=>-600, "name"=>"BOAT"}, {"amount"=>-125, "name"=>"HOUSE"}]) #=> [] 

说明

 The steps for the first example are as follows. a = @transactions.each_with_object(Hash.new { |h,k| h[k]=[] }) { |g,h| h[g["name"]] << g["amount"] } #=> {"CAR"=>[-3000], "BOAT"=>[-600, -600, -600], "HOUSE"=>[-125, -125, -125, -125]} b = a.reject { |_,v| v.size == 1 } #=> {"BOAT"=>[-600, -600, -600], "HOUSE"=>[-125, -125, -125, -125]} name, arr = b.min_by { |_,v| v.reduce(:+) } #=> ["BOAT", [-600, -600, -600]] arr #=> [-600, -600, -600] name #=> "BOAT" arr.map { |v| { "amount"=>v, "name"=>name } } #=> [{"amount"=>-600, "name"=>"BOAT"}, # {"amount"=>-600, "name"=>"BOAT"}, # {"amount"=>-600, "name"=>"BOAT"}] 

表达方式

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

使用块给定的默认值创建一个空哈希。 假设我们写

 h[:dogs] += ["Saffi"] #=> ["Saffi"] 

Ruby首先将其扩展为

 h[:dogs] = h[:dogs] + ["Saffi"] 

由于h没有键:dogsh为空),在相等的右侧为h[:dogs]调用默认值,因此表达式变为

 h[:dogs] = [] + ["Saffi"] #=> ["Saffi"] 

现在

 h #=> {:dogs=>["Saffi"]} 

表达方式

 h[:dogs] << "Saffi" #=> ["Saffi"] h #=> {:dogs=>["Saffi"]} 

类似的,因为在h[:dogs] "Saffi" []附加到空数组h[:dogs]之前, h[:dogs]设置为[] h[:dogs] 。 现在,如果我们写

 h[:dogs] << "Nina" #=> ["Saffi", "Nina"] h #=> {:dogs=>["Saffi", "Nina"]} 

由于h现在有一个键:dog因此不会调用默认块。

另一种写作方式如下。

 def doit(transactions) name, arr = transactions.each_with_object({}) { |g,h| (h[g["name"]] ||= []) << g["amount"] }. reject { |_,v| v.size == 1 }. min_by { |_,v| v.reduce(:+) } name ? arr.map { |v| { "amount"=>v, "name"=>name } } : [] end 

如果h没有键g["name"] (在这种情况下h[g["name"]] #=> nil ),则在g["amount"]之前将h[g["name"]][]附加g["amount"]

第一步。 选择“重复”交易:

 selected = @transactions.group_by { |el| el['name'] } .select{ |k, v| v.size > 1 } 

第二步。 查找具有最大金额的产品名称(在这种情况下,由于负数而最小化):

 selected.each_with_object({}) { |(k, v), obj| obj[k] = v.map { |a| a['amount'] }.sum } .min_by { |k, v| v }.first 

更新:

 @transactions.group_by { |el| el['name'] } .each_with_object({}) { |(k, v), obj| obj[k] = v.map { |a| a['amount'] }.sum if v.size > 1 } .min_by { |k, v| v }.first