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)
}
Мы можем сделать этот шаг дальше, превратив его в Перечислитель . Тогда вы можете иметь столько, сколько захотите, и делать с ними все, что захотите, используя обычные перечисляемые методы ..
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)
}
И если у вас есть особый, общий случай, вы можете написать для него особую функцию case, дать ему описательное имя и задокументировать его особое поведение.
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_day
, ни safe_change
. Ни один из них на самом деле не говорит: «Это будет использовать день, или, если последний день месяца выходит за границы», и я не уверен, как это выразить.