Search code examples
ruby-on-railsrubyruby-on-rails-5

Ruby - group_by month for current year, including months with no data


I'm trying to write a method to return a very specific data structure for a chart that I am populating.

A user can enter dates for miles that they've hiked, so on the Mile model I have start_date and end_date attributes.

The method I have currently is close, however it should return a 0 for the current month because the user hasn't entered any miles hiked for the month of May. I also need to constrain it to the current year, which it currently is not.

Here is what my current method looks like:

def miles_by_month
    miles = self.miles.from_this_year.group_by { |t| t.start_date.strftime('%b') }
    total_miles = miles.map do |m|
        { 'indicator': m[0], 'total': m[1].sum(&:length) }
    end
    total_miles
end

And the 'from_this_year' scope on the Mile model if it's helpful:

scope :from_this_year, lambda { where("start_date > ? AND start_date < ?", Time.now.beginning_of_year, Time.now.end_of_year) }

And here is an example of what it returns:

[
    [0] {
        :indicator => "Jan",
            :total => 15
    },
    [1] {
        :indicator => "Feb",
            :total => 10
    },
    [2] {
        :indicator => "Mar",
            :total => 10
    },
    [3] {
        :indicator => "Apr",
            :total => 100
    },
    [4] {
        :indicator => "May",    # I need [4] to show up with 
        :total => 0             # a total of 0. [4] currently
    }                           # does not show up at all.
]

"Indicator" refers to the name of the month, and "total" refers to a sum of the number of miles that user has hiked for that specific month.

Any suggestions?

UPDATE 1

I have modified my miles_by_month method somewhat based on an answer here to the following:

def miles_by_month
    months = Date::ABBR_MONTHNAMES[1..12]
    miles = self.miles.from_this_year.group_by { |t| t.start_date.strftime('%b') }

    total_miles = miles.map do |m|
        { 'indicator': m[0], 'total': m[1].sum(&:length) }
    end

    months.each do |month|
      unless total_miles.any? { |r| r[:indicator] == month }
        total_miles.push(indicator: month, total: 0)
      end
    end

    total_miles
  end

The only remaining thing I need to do is figure out how to constrain the months variable from the start of the year to the current month.


Solution

  • Based on Max Pleaner's answer, I was able to come up with the following method to correctly return what I needed:

    def miles_by_month
        cur_month = Time.now.month
        months = Date::ABBR_MONTHNAMES[1..cur_month]
        miles = self.miles.from_this_year.group_by { |t| t.start_date.strftime('%b') }
    
        total_miles = miles.map do |m|
            { 'indicator': m[0], 'total': m[1].sum(&:length) }
        end
    
        months.each do |month|
          unless total_miles.any? { |r| r[:indicator] == month }
            total_miles.push(indicator: month, total: 0)
          end
        end
    
        total_miles
      end
    

    This returns a data structure like the following:

    [
        [0] {
            :indicator => "Jan",
                :total => 15
        },
        [1] {
            :indicator => "Feb",
                :total => 10
        },
        [2] {
            :indicator => "Mar",
                :total => 10
        },
        [3] {
            :indicator => "Apr",
                :total => 100
        },
        [4] {
            :indicator => "May",
                :total => 0
        }
    ]
    

    It's certainly not the most efficient method and I'd be very curious to see if anyone has more efficient ways of handling this.