Iterate every month with date objects
I have added following method to Date class:
class Date
def all_months_until to
from = self
from, to = to, from if from > to
m = Date.new from.year, from.month
result = []
while m <= to
result << m
m >>= 1
end
result
end
end
You use it like:
>> t = Date.today
=> #<Date: 2009-11-12 (4910295/2,0,2299161)>
>> t.all_months_until(t+100)
=> [#<Date: 2009-11-01 (4910273/2,0,2299161)>, #<Date: 2009-12-01 (4910333/2,0,2299161)>, #<Date: 2010-01-01 (4910395/2,0,2299161)>, #<Date: 2010-02-01 (4910457/2,0,2299161)>]
Ok, so, more rubyish approach IMHO would be something along:
class Month<Date
def succ
self >> 1
end
end
and
>> t = Month.today
=> #<Month: 2009-11-13 (4910297/2,0,2299161)>
>> (t..t+100).to_a
=> [#<Month: 2009-11-13 (4910297/2,0,2299161)>, #<Month: 2009-12-13 (4910357/2,0,2299161)>, #<Month: 2010-01-13 (4910419/2,0,2299161)>, #<Month: 2010-02-13 (4910481/2,0,2299161)>]
But you would need to be careful to use first days of month (or implement such logic in Month)...
I came up with the following solution. It's a mixin for date ranges that adds an iterator for both years and months. It yields sub-ranges of the complete range.
require 'date'
module EnumDateRange
def each_year
years = []
if block_given?
grouped_dates = self.group_by {|date| date.year}
grouped_dates.each_value do |dates|
years << (yield (dates[0]..dates[-1]))
end
else
return self.enum_for(:each_year)
end
years
end
def each_month
months = []
if block_given?
self.each_year do |range|
grouped_dates = range.group_by {|date| date.month}
grouped_dates.each_value do |dates|
months << (yield (dates[0]..dates[-1]))
end
end
else
return self.enum_for(:each_month)
end
months
end
end
first = Date.parse('2009-01-01')
last = Date.parse('2011-01-01')
complete_range = first...last
complete_range.extend EnumDateRange
complete_range.each_year {|year_range| puts "Year: #{year_range}"}
complete_range.each_month {|month_range| puts "Month: #{month_range}"}
Will give you:
Year: 2009-01-01..2009-12-31
Year: 2010-01-01..2010-12-31
Month: 2009-01-01..2009-01-31
Month: 2009-02-01..2009-02-28
Month: 2009-03-01..2009-03-31
Month: 2009-04-01..2009-04-30
Month: 2009-05-01..2009-05-31
Month: 2009-06-01..2009-06-30
Month: 2009-07-01..2009-07-31
Month: 2009-08-01..2009-08-31
Month: 2009-09-01..2009-09-30
Month: 2009-10-01..2009-10-31
Month: 2009-11-01..2009-11-30
Month: 2009-12-01..2009-12-31
Month: 2010-01-01..2010-01-31
Month: 2010-02-01..2010-02-28
Month: 2010-03-01..2010-03-31
Month: 2010-04-01..2010-04-30
Month: 2010-05-01..2010-05-31
Month: 2010-06-01..2010-06-30
Month: 2010-07-01..2010-07-31
Month: 2010-08-01..2010-08-31
Month: 2010-09-01..2010-09-30
Month: 2010-10-01..2010-10-31
Month: 2010-11-01..2010-11-30
Month: 2010-12-01..2010-12-31
Here is something very Ruby:
first day of each month
(Date.new(2008, 12)..Date.new(2011, 12)).select {|d| d.day == 1}
It will give you an array of the first day for each month within the range.
last day of each month
(Date.new(2008, 12)..Date.new(2012, 01)).select {|d| d.day == 1}.map {|d| d - 1}.drop(1)
Just note that the end date needs to be the month after your end range.
I find that I need to do this sometimes when generating select lists of months. The key is the >>
operator on Date, which advances the Date forward one month.
def months_between(start_month, end_month)
months = []
ptr = start_month
while ptr <= end_month do
months << ptr
ptr = ptr >> 1
end
months
end
results = months_between(Date.new(2008,12), Date.new(2009,3))
Of course, you can format the results however you like in the loop.
months << "#{Date::MONTHNAMES[ptr.month]} #{ptr.year}"
Will return the month name and year ("March 2009"), instead of the Date object. Note that the Date objects returned will be set on the 1st of the month.