Exploring Date Manipulation in Rails

March 29, 2022
rails ruby

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:

Avoiding Test Data Mutation

October 24, 2017
ruby test rspec tdd

When to Use Ruby Threads?

Ruby threads are useful for performing remote operations such as HTTP requests, less for performing heavy code executions.
software development ruby threads mri global interpreter lock gil concurrency

React on Rails with Webpack

A short guide to creating a React application in a Rails project. Uses webpack with Babel to transpile and bundle the application.
development software react rails webpack