Reputation: 1038
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.
Any help with understanding where I've gone wrong and what to do would be greatly appreciated.
Upvotes: 2
Views: 2258
Reputation: 27793
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
{
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[2016][11][1] # => {:account_id=>135, :price=>0}
Upvotes: 0
Reputation: 46960
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. Note this function modifies the original hashes. Call on a.map{|h| h.clone}
if that's not desired. To get the example result:
group_and_index(array_of_hashes, [:year, :month])
Upvotes: 1
Reputation: 110675
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. (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
. 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
.
Upvotes: 0
Reputation: 54223
Here's a longer but possibly better solution, with 3 helper methods :
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 :
{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.
# 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.
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
}
}
}
}
Upvotes: 4