20

So I have two ruby Date objects, and I want to iterate them every month. For example if I have Date.new(2008, 12) and Date.new(2009, 3), it would yield me 2008-12, 2009-1, 2009-2, 2009-3 (as Date objects of course). I tried using range, but it yields every day. I saw step method for Date however it only allows me to pass number of days (and each month has different number of those). Anyone have any ideas?

Andrius
  • 2,768
  • 3
  • 18
  • 13

9 Answers9

71

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.

The Who
  • 6,552
  • 5
  • 36
  • 33
  • 5
    And inefficient for large date ranges – Edward Anderson Jan 10 '13 at 18:18
  • 11
    really? is 4000 years a large enough date range? Benchmark.measure {(Date.new(1, 1)..Date.new(4000, 12)).select {|d| d.day == 1}} => 1.170000 0.000000 1.170000 ( 1.181518) – The Who Jan 10 '13 at 18:32
  • I guess the only other problem is that it doesn't include the first month unless you create Date objects like you do here that start on the 1st. I like it though. – Edward Anderson Jan 10 '13 at 20:05
  • 2
    (Date.today.beginning_of_month..x.beginning_of_month).select {|d| d.day == 1} <<< Includes the first month – ar3 May 31 '13 at 15:44
  • The question says that it should return the first date, not the beginning of the month of the first date – bridiver May 09 '14 at 18:03
  • 1
    What is the difference? A new Date object without the day defaults to the first day. Date.new(2009,3) == Date.new(2009,3,1) – The Who May 09 '14 at 18:55
14

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.

Jonathan Julian
  • 12,163
  • 2
  • 42
  • 48
10

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)...

Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
6

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
Dirk Geurs
  • 2,392
  • 19
  • 24
5
MonthRange.new(date1..date2).each { |month| ... }
MonthRange.new(date1..date2).map { |month| ... }

You can use all the Enumerable methods if you use this iterator class. I make it handle strings too so that it can take form inputs.

# Iterate over months in a range
class MonthRange
  include Enumerable

  def initialize(range)
    @start_date = range.first
    @end_date   = range.last
    @start_date = Date.parse(@start_date) unless @start_date.respond_to? :month
    @end_date   = Date.parse(@end_date) unless @end_date.respond_to? :month
  end

  def each
    current_month = @start_date.beginning_of_month
    while current_month <= @end_date do
      yield current_month
      current_month = (current_month + 1.month).beginning_of_month
    end
  end
end
Fabio
  • 18,856
  • 9
  • 82
  • 114
Edward Anderson
  • 13,591
  • 4
  • 52
  • 48
3
Date.new(2014,1,1).upto(Date.today).map {|date| "#{date.to_s[0..-4]}"}.uniq

Will give you a string representation of each month including it's year.

aaron-coding
  • 2,571
  • 1
  • 23
  • 31
1

As a helper method:

def iterate(d1, d2)
  date = d1
  while date <= d2
    yield date
    date = date >> 1
  end
end

Usage:

start_date = Date.new(2008, 12)
end_date = Date.new(2009, 3)
iterate(start_date, end_date){|date| puts date}

Or, if you prefer to monkey patch Date:

class Date
  def upto(end_date)
    date = self
    while date <= end_date
      yield date
      date = date >> 1
    end
  end
end

Usage:

start_date = Date.new(2008, 12)
end_date = Date.new(2009, 3)
start_date.upto(end_date){|date| puts date}
Pablo B.
  • 1,823
  • 1
  • 17
  • 27
1

Welp, after lurking 15 years nearly this is my first stack overflow answer, I think.

start_date = Date.new(2000,12,15) # day is irrelevant and can be omitted
end_date = Date.new(2001,2,1). #same

(start_date.to_datetime..end_date.to_datetime).map{|d| [d.year, d.month]}.uniq.sort

# returns [[2000,12],[2001,1],[2001,2]]

(start_date.to_datetime..end_date.to_datetime).map{|d| Date.new(d.year, d.month)}.uniq.sort

# returns an array of date objects for the first day of any month in the span

Phil
  • 133
  • 1
  • 8
0
def each_month(date, end_date)
  ret = []
  (ret << date; date += 1.month) while date <= end_date
  ret
end
kuboon
  • 9,557
  • 3
  • 42
  • 32