简体   繁体   English

分组哈希数组

[英]Grouping an array of hashes

so I'm working on a project where I have an array of hashes: 所以我正在一个有很多哈希的项目上工作:

[{:year=>2016, :month=>12, :account_id=>133, :price=>5},
 {:year=>2016, :month=>11, :account_id=>134, :price=>3},
 {:year=>2016, :month=>11, :account_id=>135, :price=>0},
 {:year=>2015, :month=>12, :account_id=>145, :price=>4},
 {:year=>2015, :month=>12, :account_id=>163, :price=>11}]

and basically I want to condense this down into the form: 基本上我想将其浓缩为以下形式:

{ 2016 => { 12 => { 1 => {:account_id=>133, :price=>5}},
            11 => { 1 => {:account_id=>134, :price=>3},
                    2 => {:account_id=>135, :price=>0}}},
  2015 => { 12 => { 1 => {:account_id=>145, :price=>4},
                    2 => {:account_id=>163, :price=>11}}}}

but I'm having real trouble getting this done, at the moment I have: 但目前我遇到了麻烦,目前我有:

data_array = data_array.group_by{|x| x[:year]}
data_array.each{|x| x.group_by{|y| y[:month]}}

but this doesn't seem to work, I get an error saying no implicit conversion of Symbol into Integer. 但这似乎不起作用,我收到一条错误消息,指出没有将Symbol隐式转换为Integer。

Any help with understanding where I've gone wrong and what to do would be greatly appreciated. 我们将不胜感激,帮助您了解我在哪里出错了以及该怎么做。

Refactored solution 重构解决方案

Here's a longer but possibly better solution, with 3 helper methods : 这是一个更长但可能更好的解决方案,其中包含3种辅助方法:

class Array
  # Remove key from array of hashes
  def remove_key(key)
    map do |h|
      h.delete(key)
      h
    end
  end

  # Group hashes by values for given key, sort by value,
  # remove key from hashes, apply optional block to array of hashes.
  def to_grouped_hash(key)
    by_key = group_by { |h| h[key] }.sort_by { |value, _| value }
    by_key.map do |value, hashes|
      hashes_without = hashes.remove_key(key)
      new_hashes = block_given? ? yield(hashes_without) : hashes_without
      [value, new_hashes]
    end.to_h
  end

  # Convert array to indexed hash
  def to_indexed_hash(first = 0)
    map.with_index(first) { |v, i| [i, v] }.to_h
  end
end

Your script can then be written as : 您的脚本可以这样写:

data.to_grouped_hash(:year) do |year_data|
  year_data.to_grouped_hash(:month) do |month_data|
    month_data.to_indexed_hash(1)
  end
end

It doesn't need Rails or Activesupport, and returns : 它不需要Rails或Activesupport,并返回:

{2015=>
  {12=>
    {1=>{:account_id=>145, :balance=>4}, 2=>{:account_id=>163, :balance=>11}}},
 2016=>
  {11=>
    {1=>{:account_id=>134, :balance=>3}, 2=>{:account_id=>135, :balance=>0}},
   12=>{1=>{:account_id=>133, :price=>5}}}}

Refinements could be use to avoid polluting the Array class. 可以使用优化来避免污染Array类。

Original one-liner 原始一线

# require 'active_support/core_ext/hash'
# ^ uncomment in plain ruby script.

data.group_by{|h| h[:year]}
.map{|year, year_data|
  [
    year,
    year_data.group_by{|month_data| month_data[:month]}.map{|month, vs| [month, vs.map.with_index(1){|v,i| [i,v.except(:year, :month)]}.to_h]}
   .to_h]
}.to_h

It uses Hash#except from ActiveSupport. 它使用ActiveSupport 之外的Hash#

It outputs : 输出:

{
    2016 => {
        12 => {
            1 => {
                :account_id => 133,
                     :price => 5
            }
        },
        11 => {
            1 => {
                :account_id => 134,
                   :balance => 3
            },
            2 => {
                :account_id => 135,
                   :balance => 0
            }
        }
    },
    2015 => {
        12 => {
            1 => {
                :account_id => 145,
                   :balance => 4
            },
            2 => {
                :account_id => 163,
                   :balance => 11
            }
        }
    }
}

Know I'm late with this, but this problem has a beautiful recursive structure that deserves to be seen. 知道我迟到了,但是这个问题具有漂亮的递归结构,值得一看。

Inputs are the array of hashes and a list of keys to group on. 输入是哈希数组和要分组的键列表。

For the base case, the key list is empty. 对于基本情况,密钥列表为空。 Just convert the array of hashes into an index-valued hash. 只需将哈希数组转换为索引值的哈希即可。

Otherwise, use the first key in the list to accumulate a hash with corresponding input values as keys, each mapped to a list of hashes with that key deleted. 否则,使用列表中的第一个键累积具有相应输入值的哈希作为键,每个值映射到已删除该键的哈希表。 Each of these lists is just a smaller instance of the same problem using the remaining tail of keys! 这些列表中的每一个只是使用剩余键尾的同一问题的较小实例! So recur to take care of them. 因此,请重复照顾它们。

def group_and_index(a, keys)
  if keys.empty?
    a.each_with_object({}) {|h, ih| ih[ih.size + 1] = h }
  else
    r = Hash.new {|h, k| h[k] = [] }
    a.each {|h| r[h.delete(keys[0])].push(h) }
    r.each {|k, a| r[k] = group_and_index(a, keys[1..-1]) }
  end
end

If a key is missing in any of the input hashes, a nil will be used. 如果任何输入哈希中都缺少键,则将使用nil Note this function modifies the original hashes. 请注意,此功能会修改原始哈希。 Call on a.map{|h| h.clone} 调用a.map{|h| h.clone} a.map{|h| h.clone} if that's not desired. a.map{|h| h.clone}如果不需要)。 To get the example result: 要获得示例结果:

group_and_index(array_of_hashes, [:year, :month])
arr = [{:year=>2016, :month=>12, :account_id=>133, :price=>5},
       {:year=>2016, :month=>11, :account_id=>134, :price=>3},
       {:year=>2016, :month=>11, :account_id=>135, :price=>0},
       {:year=>2015, :month=>12, :account_id=>145, :price=>4},
       {:year=>2015, :month=>12, :account_id=>163, :price=>11}]

arr.each_with_object({}) do |g,h|
  f = h.dig(g[:year], g[:month])
  counter = f ? f.size+1 : 1  
  h.update(g[:year]=>{ g[:month]=>
      { counter=>{ account_id: g[:account_id], price: g[:price] } } }) { |_yr,oh,nh|
        oh.merge(nh) { |_mon,ooh,nnh| ooh.merge(nnh) } }
end
  #=> {2016=>{12=>{1=>{:account_id=>133, :price=>5}},
  #           11=>{1=>{:account_id=>134, :price=>3},
  #                2=>{:account_id=>135, :price=>0}}
  #          },
  #    2015=>{12=>{1=>{:account_id=>145, :price=>4},
  #                2=>{:account_id=>163, :price=>11}}
  #          }
  #   }

This uses the methods Hash#dig and the forms of Hash#update (aka merge! ) and Hash#merge that employ a block to determine the values of keys that are present in both hashes being merged. 这使用方法Hash#dig以及Hash#update (也称为merge! )和Hash#merge的形式 ,后者采用一个块来确定要合并的两个哈希中存在的键的值。 (See the docs for details.) Note that there are such blocks at two difference levels. (有关详细信息,请参阅文档。)请注意,存在两个不同级别的此类块。 If, for example, 例如,如果

{ 2016=>{ 11=>{ {1=>{:account_id=>133, :price=>5 } } } } }
{ 2016=>{ 11=>{ {2=>{:account_id=>135, :price=>0 } } } } }

are being merged, the block would determine the value of 2016 . 被合并时,该区块将确定2016的值。 That involves merging the two hashes 这涉及合并两个哈希

{ 11=>{ {1=>{:account_id=>133, :price=>5 } } } }
{ 11=>{ {2=>{:account_id=>135, :price=>0 } } } }

which would call an inner block to determine the value of 11 . 这将调用一个内部块来确定11的值。

Here is a simple two-liner 这是一个简单的两层

h = Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = [] }}
ary.each { |each| h[each.delete(:year)][each.delete(:month)] << each }

NB, this modifies the input but I assume you are not interested in the original input after transforming it. 注意,这会修改输入,但是我认为您对原始输入进行了转换后不感兴趣。

Value of h h

{
  2016=>{12=>[{:account_id=>133, :price=>5}], 11=>[{:account_id=>134, :price=>3}, {:account_id=>135, :price=>0}]},
  2015=>{12=>[{:account_id=>145, :price=>4}, {:account_id=>163, :price=>11}]}
}

You can access the values in h with 您可以使用以下命令访问h的值

h[2016][11][1] # => {:account_id=>135, :price=>0}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM