user138016
user138016

Reputation:

How to make dynamic multi-dimensional array in ruby?

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

Answers (4)

Michael Sepcot
Michael Sepcot

Reputation: 11385

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

Upvotes: 7

Sinan Taifour
Sinan Taifour

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

sepp2k
sepp2k

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

ChrisInEdmonton
ChrisInEdmonton

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

Related Questions