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
的单个值时,你可能需要reduce
或inject
。
有关详细信息,请参阅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
没有键:dogs
( h
为空),在相等的右侧为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