Поэтому я работаю над проектом, в котором у меня есть массив хэшей:

[{: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}]

И в основном я хочу сжать это до формы:

{ 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}}}}

Но у меня настоящие проблемы с этим, на данный момент у меня есть:

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

Но это, похоже, не работает, я получаю сообщение об отсутствии неявного преобразования символа в целое число.

Любая помощь в понимании того, где я ошибся и что делать, будет принята с благодарностью.

2
user2320239 22 Дек 2016 в 18:30
Какое это имеет отношение к ruby-on-rails? Вы получаете это в результате какого-то веб-запроса? Можете ли вы изменить формат веб-запроса?
 – 
Jörg W Mittag
22 Дек 2016 в 18:34
Извините, я просто поставил Ruby-on-Rails, потому что это проект для рельсов
 – 
user2320239
22 Дек 2016 в 18:36
1
Вы пробовали просто делать это долгим путем? пройтись по вашему массиву с помощью .each. Затем вы можете обработать каждый хеш отдельно и поместить его в новый хеш. Сначала заставьте это работать долгий путь.
 – 
MageeWorld
22 Дек 2016 в 18:46

4 ответа

Лучший ответ

Реорганизованное решение

Вот более длинное, но, возможно, лучшее решение с 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

Ваш сценарий может быть записан как:

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

Ему не нужны 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}}}}

Можно использовать уточнения, чтобы избежать загрязнения класса Array.

Оригинальный однострочный

# 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

Он использует Hash # за исключением из ActiveSupport.

Он выводит:

{
    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
            }
        }
    }
}
4
Eric Duminil 22 Дек 2016 в 22:22
Спасибо за ответ, хотя когда я запускаю это, я получаю сообщение об ошибке undefined method `except 'Я включил активную поддержку.
 – 
user2320239
22 Дек 2016 в 19:05
Он должен работать в Rails. В простом Ruby вы можете использовать require 'active_support/core_ext/hash'
 – 
Eric Duminil
22 Дек 2016 в 19:07

Знаю, я опаздываю с этим, но у этой проблемы есть красивая рекурсивная структура, которая заслуживает внимания.

Входные данные - это массив хэшей и список ключей для группировки.

Для базового случая список ключей пуст. Просто преобразуйте массив хешей в хеш с индексным значением.

В противном случае используйте первый ключ в списке для накопления хэша с соответствующими входными значениями в качестве ключей, каждый из которых сопоставлен со списком хэшей с удаленным ключом. Каждый из этих списков - всего лишь меньший по размеру экземпляр той же проблемы с использованием оставшегося хвоста ключей! Так что вернитесь, чтобы позаботиться о них.

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

Если ключ отсутствует в любом из входных хэшей, будет использоваться nil. Обратите внимание, что эта функция изменяет исходные хэши. Если это нежелательно, позвоните a.map{|h| h.clone}. Чтобы получить результат примера:

group_and_index(array_of_hashes, [:year, :month])
1
Gene 24 Дек 2016 в 09:53
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}}
  #          }
  #   }

При этом используются методы Hash # dig и формы Hash # update (также известного как merge!) и Hash # merge , которые используют блок для определения значений ключей, которые присутствуют в обоих сливаемых хэшах. (Подробности см. В документации.) Обратите внимание, что есть такие блоки на двух уровнях различия. Если, например,

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

Объединяются, блок будет определять значение 2016. Это включает в себя объединение двух хешей

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

Который вызовет внутренний блок для определения значения 11.

0
Cary Swoveland 23 Дек 2016 в 10:42

Вот простой двухстрочный

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

Обратите внимание, это изменяет ввод, но я предполагаю, что вас не интересует исходный ввод после его преобразования.

Значение 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}]}
}

Вы можете получить доступ к значениям в h с помощью

h[2016][11][1] # => {:account_id=>135, :price=>0}
0
akuhn 25 Дек 2016 в 11:02
1
Почему отрицательные голоса? Лучше всего оставить комментарий, чтобы я мог улучшить ответ.
 – 
akuhn
24 Дек 2016 в 03:03