Group a Ruby Array of Dates by Month and Year into

2020-07-23 06:01发布

Let's say I had a Ruby Array of Dates like:

2011-01-20
2011-01-23
2011-02-01
2011-02-15
2011-03-21

What would be an easy and Ruby-esque way of creating a Hash that groups the Date elements by year and then month, like:

{
    2011 => {
        1   => [2011-01-20, 2011-01-23],
        2   => [2011-02-01, 2011-02-15],
        3  => [2011-03-21],
    }
}

I can do this by iterating over everything and extracting years, months and so on, then comining them.

Ruby offers so many methods and blocks for Arrays and Hashes, there must be an easier way?

3条回答
Deceive 欺骗
2楼-- · 2020-07-23 06:29
dates = %w(
2011-01-20
2011-01-23
2011-02-01
2011-02-15
2011-03-21
)
hash = {}
dates.each do |date|
   year, month = date.strftime('%Y,%B').split(',')
   hash[year] ||= {}
   hash[year][month] = hash[year][month].to_a << date
end
查看更多
小情绪 Triste *
3楼-- · 2020-07-23 06:33
require 'date'

dates = [
  '2011-01-20',
  '2011-01-23',
  '2011-02-01',
  '2011-02-15',
  '2011-03-21'
].map{|sd| Date.parse(sd)}

Hash[
  dates.group_by(&:year).map{|y, items|
    [y, items.group_by{|d| d.strftime('%B')}]
  }
]
#=> {2011=>{"January"=>[#<Date: 2011-01-20 (4911163/2,0,2299161)>, #<Date: 2011-01-23 (4911169/2,0,2299161)>], "February"=>[#<Date: 2011-02-01 (4911187/2,0,2299161)>, #<Date: 2011-02-15 (4911215/2,0,2299161)>], "March"=>[#<Date: 2011-03-21 (4911283/2,0,2299161)>]}} 

I noticed you have changed month names into numbers, so you may want to replace d.strftime('%B') above with d.month or whatever else.

Here's a step-by-step explanation:

You essentially want two-level grouping: first level by year, second by month. Ruby has very useful method group_by, which groups elements by given expression (a block). So: first part is grouping original array by year:

hash_by_year = dates.group_by(&:year)
# => {2011=>[#<Date: 2011-01-20 (4911163/2,0,2299161)>, #<Date: 2011-01-23 (4911169/2,0,2299161)>, #<Date: 2011-02-01 (4911187/2,0,2299161)>, #<Date: 2011-02-15 (4911215/2,0,2299161)>, #<Date: 2011-03-21 (4911283/2,0,2299161)>]}

That gives us first level: keys are years, values arrays of dates with given year. But we still need to group the second level: that's why we map by-year hash - to group its values by month. Let's for start forget strftime and say that we're grouping by d.month:

hash_by_year.map{|year, dates_in_year|
  [year, dates_in_year.group_by(&:month)]
}
# => [[2011, {1=>[#<Date: 2011-01-20 (4911163/2,0,2299161)>, #<Date: 2011-01-23 (4911169/2,0,2299161)>], 2=>[#<Date: 2011-02-01 (4911187/2,0,2299161)>, #<Date: 2011-02-15 (4911215/2,0,2299161)>], 3=>[#<Date: 2011-03-21 (4911283/2,0,2299161)>]}]]

That way we got our second level grouping. Instead of array of all dates in a year, now we have hash whose keys are months, and values arrays of dates for a given month.

The only problem we have is that map returns an array and not a hash. Thats why we "surround" whole expression by Hash[], which makes a hash out of array of pairs, in our case pairs [year, hash_of_dates_by_month].

Sorry if the explanation sounds confusing, I found harder to explain functional expressions than imperative, because of the nesting. :(

查看更多
爷的心禁止访问
4楼-- · 2020-07-23 06:49

This gets you pretty close, you just need to change the numerical month number into a textual month name:

dates = %w(
2011-01-20
2011-01-23
2011-02-01
2011-02-15
2011-03-21
)

grouped = dates.inject({}) do |ret, date|
  y,m,d = date.split('-')
  ret[y] ||= {}
  # Change 'm' into month name here
  ret[y][m] ||= []
  ret[y][m] << date
  ret
end

puts grouped.inspect
查看更多
登录 后发表回答