安全更改红宝石日期日期的优雅方式?

时间:2019-01-25 23:16:49

标签: ruby-on-rails ruby date

我必须创建一个包含24个day的月份列表,并正确处理没有29、30或31天的月份。

我目前要做的是:

def dates_list(first_month, assigned_day)
 (0...24).map do |period|
    begin
      (first_month + period.months).change(day: assigned_day)
    rescue ArgumentError
      (first_month + period.months).end_of_month
    end
  end
end

在某些情况下,我需要从ArgumentError营救下来:

Date.parse('10-Feb-2019').change(day: 30)
# => ArgumentError: invalid date

我正在寻找可能已经存在于红宝石或铁轨中的 safe elegant 解决方案。像这样:

Date.parse('10-Feb-2019').safe_change(day: 30) # => 28-Feb-2019

所以我可以写:

def dates_list(first_month, assigned_day)
  (0...24).map do |period|
    (first_month + period.months).safe_change(day: assigned_day)
  end
end

存在吗,或者我需要猴子打Date

非常欢迎变通方法(例如已经创建此列表的方法)。

1 个答案:

答案 0 :(得分:2)

UPDATE

关于如何处理负数和0天的讨论,使我意识到此功能试图猜测用户的意图。而且还可以硬编码要生成多少个月以及要按月生成。

这让我开始思考这种方法在做什么?它会生成一个固定大小的前进月份列表,并以固定方式对其进行修改,然后猜测用户的需求。如果您的功能说明包含“ ”,则可能需要多个功能。我们将生成日期列表与修改列表分开。我们用参数替换硬编码部分。我们不用猜测用户想要什么,而是让他们告诉我们。

def date_generator(from, by:, how_many:)
  (0...how_many).map do |period|
    date = from + period.send(by)
    yield date
  end
end

用户可以非常明确地了解他们想要更改的内容。用户和读者都不会感到惊讶。

p date_generator(Date.parse('2019-02-01'), by: :month, how_many: 24) { |month|
  month.change(day: month.end_of_month.day)
}

我们可以通过将其变成Enumerator来更进一步。然后,您可以拥有任意数量的对象,并使用普通的Enumerable methods对它们进行任何操作。.

INFINITY = 1.0/0.0
def date_iterator(from, by:)
  Enumerator.new do |block|
    (0..INFINITY).each do |period|
      date = from + period.send(by)
      block << date
    end
  end
end

p date_iterator(Date.parse('2019-02-01'), by: :month)
    .take(24).map { |date|
      date.change(day: date.end_of_month.day)
    }

现在,您可以生成任何日期列表,可以按任意字段,任意长度,进行任何更改进行迭代。对于读者来说,发生的事情不是隐藏在方法中,而是非常明显的。而且,如果您有特殊的普通情况,可以将其包装在方法中。

最后一步是使其成为Date方法。

class Date
  INFINITY = 1.0/0.0
  def iterator(by:)
    Enumerator.new do |block|
      (0..INFINITY).each do |period|
        date = self + period.send(by)
        block << date
      end
    end
  end
end

Date.parse('2019-02-01')
  .iterator(by: :month)
  .take(24).map { |date|
    date.change(day: date.end_of_month.day)
  }

如果您有一个特殊的普通案例,则可以为其编写一个特殊案例函数,为其指定一个描述性名称,并记录其特殊行为。

def next_two_years_of_months(date, day:)
  if day <= 0
    raise ArgumentError, "The day must be positive"
  end

  date.iterator(by: :month)
    .take(24)
    .map { |next_date|
      next_date.change(day: [day, next_date.end_of_month.day].min)
    }
end

上级答案

我的第一个重构就是删除冗余代码。

require 'date'

def dates_list(first_month, assigned_day)
 (0...24).map do |period|
   next_month = first_month + period.months
   begin
     next_month.change(day: assigned_day)
   rescue ArgumentError
     next_month.end_of_month
   end
 end
end

在这一点上,imo,功能很好。很明显发生了什么事。但是您可以更进一步。

def dates_list(first_month, assigned_day)
  (0...24).map do |period|
    next_month = first_month + period.months
    day = [assigned_day, next_month.end_of_month.day].min
    next_month.change(day: day)
  end
end

我认为这要好得多。它使决策更加明确,并且不会覆盖其他可能的参数错误。

如果您发现自己经常这样做,则可以将其添加为Date方法。

class Date
  def change_day(day)
    change(day: [day, end_of_month.day].min)
  end
end

我对change_daysafe_change都不太感兴趣。并没有真正说“会用到这一天,或者如果它超出了本月的最后一天”,我不确定如何表达。