If you have worked extensively with Rails, you probably have seen date being manipulated such as Date.today + 1.month
. Have you ever wondered what is 1.month
? Is it 30 days, 31 days, 28 days or even 29 days on leap years?
1.month.in_days
=> 30.436875
2.months.in_days
=> 60.87375
It turns out that 1 month is 30.436875 days according to Rails.
Rails also gives us a convenient way of adding a duration to a Date-like object.
Time.zone.parse('2022-01-01') + 1.month
# => '2022-02-01'
Time.zone.parse('2022-02-01') + 1.month
# => '2022-03-01'
Time.zone.parse('2022-04-01') + 1.month
# => '2022-05-01'
1.month
seems to know how many days there are in the current month and adapt accordingly. So how does it do that? From what we saw earlier, 1.month.in_days
is 30.436875 days.
The first thing that comes in mind is that most probably 1.month
does not immediately return actual number of days. It seems to be an object that is just added to Date
and Date
knows how many days to add depending on its current month.
class ExtendedDate
def add(months)
current_month = @month
monts_to_add = months.value
days_to_add = 0
until months_to_add == 0
days_to_add += days_in_month(month)
months_to_add -= 1
current_month += 1
end
self + days_to_add
end
def days_in_month(month)
case month
when 1 then 31
when 2 then 28
when 3 then 31
when 4 then 30
# ...
end
end
end
class Month
attr_reader :value
def initialize(value)
@value = value
end
end
ExtendedDate.new(2022, 2, 1).add(Month.new(1))
# => 2022-03-01
This works. However, that is a lot of knowledge about number of days in just Date
class. The knowledge of number of days seems to be more in line with what a Month
should know. Let’s apply a more object oriented approach to this. The class Month can respond to a message sent by Date
, to provide it with the relevant number of days.
Imagine we could ask the class Month, “How many days from today is 1 month from now?”. To answer that, Month would ask in return “What date is it?”. So let’s provide the date to Month.
class Month
attr_reader :value
def initialize(value)
@value = value
end
def days_since(date)
months = value
current_month = date.month
days = 0
until months == 0
days += days_in_month(month)
months -= 1
current_month += 1
end
days
end
def days_in_month(month)
case month
when 1 then 31
when 2 then 28
when 3 then 31
when 4 then 30
# ...
end
end
end
Now Month can return the appropriate number of days. So how do we use it to add to the Date?
class Date
def add(months)
self + months.days_since(self)
end
end
Now the behaviour of determining the number of days in a month is contained within the class Month
.
For the actual Rails implementation, have a look at the following links:
- https://github.com/rails/rails/blob/de53ba56cab69fb9707785a397a59ac4aaee9d6f/activesupport/lib/active_support/time_with_zone.rb#L328
- https://github.com/rails/rails/blob/de53ba56cab69fb9707785a397a59ac4aaee9d6f/activesupport/lib/active_support/core_ext/date/calculations.rb#L90
- https://github.com/rails/rails/blob/de53ba56cab69fb9707785a397a59ac4aaee9d6f/activesupport/lib/active_support/duration.rb#L430
- https://github.com/rails/rails/blob/de53ba56cab69fb9707785a397a59ac4aaee9d6f/activesupport/lib/active_support/core_ext/date/calculations.rb#L61