tags:

views:

1287

answers:

4

Hey,

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

Thank you.

+5  A: 

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
Sinan Taifour
Thanks for the enlightening me.
incidence
You're welcome. Look at Michael_Sepcot's answer for how to do in a more _rubyish_ way.
Sinan Taifour
+3  A: 

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

ChrisInEdmonton
+4  A: 
# 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
sepp2k
+5  A: 

You can get the nested array structure in one line by using a combination of group_bys and map:

@entries.group_by {|entry| entry.created_at.year }.map { |year, entries| [year, entries.group_by {|entry| entry.created_at.month }] }
Michael Sepcot
Isn't group_by a rails addition, not plain old ruby?
Andrew Grimm
No, it comes in Ruby 1.8.7.
Sinan Taifour
Ah - I was trying it on Ruby 1.8.6 .
Andrew Grimm