基于键合并散列数组中的散列值

我有一系列与此类似的哈希:

[ {"student": "a","scores": [{"subject": "math","quantity": 10},{"subject": "english", "quantity": 5}]}, {"student": "b", "scores": [{"subject": "math","quantity": 1 }, {"subject": "english","quantity": 2 } ]}, {"student": "a", "scores": [ { "subject": "math", "quantity": 2},{"subject": "science", "quantity": 5 } ] } ] 

有没有更简单的方法来获得类似于此的输出,除了循环数组并找到重复然后组合它们?

 [ {"student": "a","scores": [{"subject": "math","quantity": 12},{"subject": "english", "quantity": 5},{"subject": "science", "quantity": 5 } ]}, {"student": "b", "scores": [{"subject": "math","quantity": 1 }, {"subject": "english","quantity": 2 } ]} ] 

合并重复对象的规则:

  • 学生在匹配“价值”时合并(例如学生“a”,学生“b”)
  • 添加相同科目的学生成绩(例如,合并时学生a的数学成绩2和10成为12)

有没有更简单的方法来获得类似于此的输出,除了循环数组并找到重复然后组合它们?

从来没听说过。 如果您解释这些数据的来源,答案可能会有所不同,但只是基于Hash对象Array ,我认为您将需要迭代和组合。

虽然它不优雅,但您可以使用这样的解决方案

 arr = [ {"student"=> "a","scores"=> [{"subject"=> "math","quantity"=> 10},{"subject"=> "english", "quantity"=> 5}]}, {"student"=> "b", "scores"=> [{"subject"=> "math","quantity"=> 1 }, {"subject"=> "english","quantity"=> 2 } ]}, {"student"=> "a", "scores"=> [ { "subject"=> "math", "quantity"=> 2},{"subject"=> "science", "quantity"=> 5 } ] } ] #Group the array by student arr.group_by{|student| student["student"]}.map do |student_name,student_values| {"student" => student_name, #combine all the scores and group by subject "scores" => student_values.map{|student| student["scores"]}.flatten.group_by{|score| score["subject"]}.map do |subject,subject_values| {"subject" => subject, #combine all the quantities into an array and reduce using `+` "quantity" => subject_values.map{|h| h["quantity"]}.reduce(:+) } end } end #=> [ {"student"=>"a", "scores"=>[ {"subject"=>"math", "quantity"=>12}, {"subject"=>"english", "quantity"=>5}, {"subject"=>"science", "quantity"=>5}]}, {"student"=>"b", "scores"=>[ {"subject"=>"math", "quantity"=>1}, {"subject"=>"english", "quantity"=>2}]} ] 

我知道您指定了预期的结果,但我想指出使输出更简单使代码更简单。

  arr.map(&:dup).group_by{|a| a.delete("student")}.each_with_object({}) do |(student, scores),record| record[student] = scores.map(&:values).flatten.map(&:values).each_with_object(Hash.new(0)) do |(subject,score),obj| obj[subject] += score obj end record end #=>{"a"=>{"math"=>12, "english"=>5, "science"=>5}, "b"=>{"math"=>1, "english"=>2}} 

通过这种结构,让学生像调用.keys一样简单,分数也同样简单。 我在想类似的东西

 above_result.each do |student,scores| puts student scores.each do |subject,score| puts " #{subject.capitalize}: #{score}" end end end 

控制台输出将是

 a Math: 12 English: 5 Science: 5 b Math: 1 English: 2 

在这种情况下,有两种常见的聚合值的方法。 第一种是使用方法Enumerable#group_by ,正如@engineersmnky在他的回答中所做的那样。 第二种是使用方法Hash #update (aka merge! )的forms构建哈希,该方法使用块来解析在合并的两个哈希中存在的键的值。 我的解决方案使用后一种方法,不是因为我更喜欢它而不是group_by ,而只是为了向您展示它可以采用的不同方式。 (如果工程师使用update ,我会使用group_by 。)

您使用的特定数据结构会使您的问题变得复杂。 我发现通过首先将数据转换为不同的结构,更新分数,然后将结果转换回数据结构,可以简化解决方案并使其更容易理解。 您可能需要考虑更改数据结构(如果这是您的选项)。 我在“讨论”部分讨论了这个问题。

 def combine_scores(arr) reconstruct(update_scores(simplify(arr))) end def simplify(arr) arr.map do |h| hash = Hash[h[:scores].map { |g| g.values }] hash.default = 0 { h[:student]=> hash } end end def update_scores(arr) arr.each_with_object({}) do |g,h| h.update(g) do |_, h_scores, g_scores| g_scores.each { |subject,score| h_scores[subject] += score } h_scores end end end def reconstruct(h) h.map { |k,v| { student: k, scores: v.map { |subject, score| { subject: subject, score: score } } } } end 

 arr = [ { student: "a", scores: [{ subject: "math", quantity: 10 }, { subject: "english", quantity: 5 }] }, { student: "b", scores: [{ subject: "math", quantity: 1 }, { subject: "english", quantity: 2 } ] }, { student: "a", scores: [{ subject: "math", quantity: 2 }, { subject: "science", quantity: 5 } ] }] combine_scores(arr) #=> [{ :student=>"a", # :scores=>[{ :subject=>"math", :score=>12 }, # { :subject=>"english", :score=> 5 }, # { :subject=>"science", :score=> 5 }] }, # { :student=>"b", # :scores=>[{ :subject=>"math", :score=> 1 }, # { :subject=>"english", :score=> 2 }] }] 

说明

首先考虑两个中间计算:

 a = simplify(arr) #=> [{ "a"=>{ "math"=>10, "english"=>5 } }, # { "b"=>{ "math"=> 1, "english"=>2 } }, # { "a"=>{ "math"=> 2, "science"=>5 } }] h = update_scores(a) #=> {"a"=>{"math"=>12, "english"=>5, "science"=>5} # "b"=>{"math"=> 1, "english"=>2}} 

然后

 reconstruct(h) 

返回上面显示的结果。

+ 简化

 arr.map do |h| hash = Hash[h[:scores].map { |g| g.values }] hash.default = 0 { h[:student]=> hash } end 

这会将每个哈希映射为更简单的哈希。 例如, arr的第一个元素:

 h = { student: "a", scores: [{ subject: "math", quantity: 10 }, { subject: "english", quantity: 5 }] } 

映射到:

 { "a"=>Hash[[{ subject: "math", quantity: 10 }, { subject: "english", quantity: 5 }].map { |g| g.values }] } #=> { "a"=>Hash[[["math", 10], ["english", 5]]] } #=> { "a"=>{"math"=>10, "english"=>5}} 

将每个哈希的默认值设置为零简化了后续的更新步骤。

+ update_scores

对于由simplify返回的哈希数组,我们计算:

 a.each_with_object({}) do |g,h| h.update(g) do |_, h_scores, g_scores| g_scores.each { |subject,score| h_scores[subject] += score } h_scores end end 

a (哈希)的每个元素合并为最初为空的哈希值h 。 由于update (与merge!相同)用于合并,因此修改了h 。 如果两个哈希值共享相同的键(例如,“math”),则将值相加; else subject=>score加到h

请注意,如果h_scores没有关键subject ,则:

 h_scores[subject] += score #=> h_scores[subject] = h_scores[subject] + score #=> h_scores[subject] = 0 + score (because the default value is zero) #=> h_scores[subject] = score 

也就是说,来自g_scores的键值对仅添加到h_scores

我用占位符_替换了代表主题的块变量,以减少出错的可能性并告知读者它没有在块中使用。

+ 重建

最后一步是将update_scores返回的哈希值转换回原始数据结构,这很简单。

讨论

如果您更改了数据结构并且它符合您的要求,您可能希望考虑将其更改为combine_scores生成的数据结构:

 h = { "a"=>{ math: 10, english: 5 }, "b"=>{ math: 1, english: 2 } } 

然后用以下内容更新分数:

 g = { "a"=>{ math: 2, science: 5 }, "b"=>{ english: 3 }, "c"=>{ science: 4 } } 

你只想做到以下几点:

 h.merge(g) { |_,oh,nh| oh.merge(nh) { |_,ohv,nhv| ohv+nhv } } #=> { "a"=>{ :math=>12, :english=>5, :science=>5 }, # "b"=>{ :math=> 1, :english=>5 }, # "c"=>{ :science=>4 } }