Reputation:
I have a beginner ruby question about multi dimensional arrays.
I want to sort entries by year and month. So I want to create a multi-dimensional array that would contain years -> months -> entries of month
So the array would be like:
2009 ->
08
-> Entry 1
-> Entry 2
09
-> Entry 3
2007 ->
10
-> Entry 5
Now I have:
@years = []
@entries.each do |entry|
timeobj = Time.parse(entry.created_at.to_s)
year = timeobj.strftime("%Y").to_i
month = timeobj.strftime("%m").to_i
tmparr = []
tmparr << {month=>entry}
@years.push(year)
@years << tmparr
end
but when I try to iterate through the years array, I get: "undefined method `each' for 2009:Fixnum"
Tried also:
@years = []
@entries.each do |entry|
timeobj = Time.parse(entry.created_at.to_s)
year = timeobj.strftime("%Y").to_i
month = timeobj.strftime("%m").to_i
@years[year][month] << entry
end
Upvotes: 7
Views: 12756
Reputation: 11385
You can get the nested array structure in one line by using a combination of group_by
s and map
:
@entries.group_by {|entry| entry.created_at.year }.map { |year, entries| [year, entries.group_by {|entry| entry.created_at.month }] }
Upvotes: 7
Reputation: 10795
You are getting the error because a FixNum
(that is, a number) is pushed on the array, in the line that reads @years.push(year)
.
Your approach of using Arrays to start with is a bit flawed; an array is perfect to hold an ordered list of items. In your case, you have a mapping from keys to values, which is perfect for a Hash.
In the first level, the keys are years, the values are hashes. The second level's hashes contain keys of months, and values of arrays of entries.
In this case, a typical output of your code would look something like (based on your example):
{ 2009 => { 8 => [Entry1, Entry2], 9 => [Entry3] }, 2007 => { 10 => [Entry5] }}
Notice that, however, the order of years and months is not guaranteed to be in any particular order. The solution is normally to order the keys whenever you want to access them. Now, a code that would generate such an output (based on your layout of code, although can be made much rubier):
@years = {}
@entries.each do |entry|
timeobj = Time.parse(entry.created_at.to_s)
year = timeobj.strftime("%Y").to_i
month = timeobj.strftime("%m").to_i
@years[year] ||= {} # Create a sub-hash unless it already exists
@years[year][month] ||= []
@years[year][month] << entry
end
Upvotes: 10
Reputation: 370102
# create a hash of hashes of array
@years = Hash.new do |h,k|
h[k] = Hash.new do |sh, sk|
sh[sk] = []
end
end
@entries.each do |entry|
timeobj = Time.parse(entry.created_at.to_s)
year = timeobj.year
month = timeobj.month
@years[year][month] << entry
end
Upvotes: 3
Reputation: 4578
I'm using hash tables instead of arrays, because I think it probably makes more sense here. However, it's fairly trivial to change back to using arrays if that's what you prefer.
entries = [
[2009, 8, 1],
[2009, 8, 2],
[2009, 9, 3],
[2007, 10, 5]
]
years = Hash.new
entries.each { |e|
year = e[0]
month = e[1]
entry = e[2]
# Add to years array
years[year] ||= Hash.new
years[year][month] ||= Array.new
years[year][month] << entry
}
puts years.inspect
The output is: {2007=>{10=>[5]}, 2009=>{8=>[1, 2], 9=>[3]}}
Upvotes: 3