2009-11-12 3 views
17

Итак, у меня есть два объекта Ruby Date, и я хочу их повторять каждый месяц. Например, если у меня есть Date.new (2008, 12) и Date.new (2009, 3), это даст мне 2008-12, 2009-1, 2009-2, 2009-3 (как, например, объекты Date). Я пробовал использовать диапазон, но он дает каждый день. Я видел метод шагов для Date, но он позволяет мне пропускать только количество дней (и каждый месяц имеет другое число). У кого-нибудь есть идеи?Итерации каждый месяц с объектами даты

ответ

10

Я добавил следующий метод Date класса:

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 

Вы можете использовать это нравится:

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

Итак, более rubyish подход ИМХО было бы что-то по:

class Month<Date 
    def succ 
    self >> 1 
    end 
end 

и

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

Но вы должны быть осторожны, чтобы использовать первые дни месяца (или реализовать такую ​​логику в месяц) ...

+0

Надеюсь на что-то более рубиново ... – Andrius

+2

Ну, единственный «рубиновый» подход, который приходит мне на ум, будет определять класс «месяц» (путем наследования даты), определяющий метод succ и использование Range на нем. –

+0

Спасибо Младен! Этот метод all_month_until был именно тем, что я искал. – jspooner

9

Я считаю, что мне нужно сделать это иногда при создании списков выберите месяцев. Ключом является оператор >> по дате, который продвигает дату вперед на один месяц.

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

Конечно, вы можете отформатировать результаты, как вам нравится в цикле.

months << "#{Date::MONTHNAMES[ptr.month]} #{ptr.year}" 

Вернет название месяца и год («Март 2009») вместо объекта «Дата». Обратите внимание, что возвращенные объекты Date будут установлены в 1-ом месяце.

+1

Это единственный действительный ответ здесь, поскольку он объясняет 'Date # >>' – mikezter

6

Я придумал следующее решение. Это смесь для диапазонов дат, которая добавляет итератор для обоих лет и месяцев. Он дает поддиапазоны полного диапазона.

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

Даст вам:

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 
0

Как вспомогательный метод:

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

Использование:

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

Или, если вы предпочитаете обезьяны патч Дата:

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

Использование:

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

Вот что-то очень Ruby:

первый день каждого месяца

(Date.new(2008, 12)..Date.new(2011, 12)).select {|d| d.day == 1} 

Это даст вам массив в первый день для каждый месяц в пределах диапазона.

последний день каждого месяца

(Date.new(2008, 12)..Date.new(2012, 01)).select {|d| d.day == 1}.map {|d| d - 1}.drop(1) 

Сразу отметим, что дата окончания должна быть месяц после конца диапазона.

+1

Это самый элегантный! –

+5

И неэффективен для больших диапазонов дат –

+9

действительно? 4000 лет достаточно большой диапазон дат? Benchmark.measure {(Date.new (1, 1) .. Date.new (4000, 12)). Select {| d | d.day == 1}} => 1.170000 0.000000 1.170000 (1.181518) –

5
MonthRange.new(date1..date2).each { |month| ... } 
MonthRange.new(date1..date2).map { |month| ... } 

Вы можете использовать все методы Enumerable, если вы используете этот класс итератора. Я также обрабатываю строки так, чтобы они могли принимать входные данные формы.

# 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 
+1

Для begin_of_month вам нужен ActiveSupport (от рельсов) –

0
def each_month(date, end_date) 
    ret = [] 
    (ret << date; date += 1.month) while date <= end_date 
    ret 
end 
1
Date.new(2014,1,1).upto(Date.today).map {|date| "#{date.to_s[0..-4]}"}.uniq 

Даст вам строковое представление каждого месяца, включая его год.

Смежные вопросы