In Progress
Unit 1, Lesson 1
In Progress

# Date Math

Video transcript & code

If you're used to working on Rails applications, you're probably used to doing various date and time calculations using the ActiveSupport extensions.

For instance, calculating the date a year ago today.

Or two weeks into the future.

``````require "date"
require "active_support/core_ext/time"

Time.now.next_year              # => 2017-07-22 12:31:12 -0400
Time.now.weeks_ago(2)           # => 2016-07-08 12:31:12 -0400
``````

If, after using all of these convenient methods, you found yourself working on a non-Rails project, you might have been rudely surprised to find that Time is a much less helpful object on its own.

``````Time.now.next_year              # => NoMethodError: undefined method `next_year' for 2016-07-22 12:32:23 -0400:Time

# ~> NoMethodError
# ~> undefined method `next_year' for 2016-07-22 12:32:23 -0400:Time
# ~>
# ~> xmptmp-in16026-RP.rb:1:in `<main>'
``````

The most help we get for doing date calculations from the core `Time` class is the ability to add or subtract a number of seconds.

So, for instance, we might try to find the date a year ago today by calculating the number of seconds in a year, and then subtracting that number.

``````Time.now - (365 * 24 * 60 * 60)
# => 2015-07-23 12:43:00 -0400
``````

But this is ugly, and it's also a technique that's prone to inaccuracies. For instance, it doesn't take into account leap years or leap seconds.

The truth is, Ruby's core `Time` class isn't much more than a thin wrapper over the UNIX concept of a timestamp: a point in time defined by a count of seconds since the UNIX epoch.

We can see this reflected in the fact that it has a #to_i conversion which returns this internal counter.

What is this mysterious Epoch that `Time` objects are counting up from? We can find out by creating a new `Time` at the zero point and stripping it of the local time zone.

``````Time.now.to_i                   # => 1469205406
Time.at(0).utc                  # => 1970-01-01 00:00:00 UTC
``````

As we can see, the UNIX Epoch is January 1, 1970.

Ruby does have a class that's dedicated to doing more advanced date calculations; it's just not the `Time` class.

It's the Date class, contained in the `date` standard library.

Just as there's a Time.now, there's a Date.today.

But chances are, in most cases we'll already have a `Time` object to work with. Even if that object really represents a date.

We can convert a preexisting `Time` to a `Date` using the #to_date method that the `date` library adds.

Then, we can calculate the date a year in the past using prev_year.

If we need more than one year in the past, we can pass a count.

Similarly, there are methods for moving to the past or future by months.

There's also a shortcut for month math: the left-shift and right-shift operators are overloaded to offset a `Date` by a number of years into the past or future.

…and by days.

Day calculations have another shortcut: the addition and subtraction operators.

Once we have the date we want, we can turn it back into a Time using #to_time.

``````require "date"

Date.today                      # => #<Date: 2016-07-22 ((2457592j,0s,0n),+0s,2299161j)>
t = Time.new(2016, 7, 22)       # => 2016-07-22 00:00:00 -0400
d = t.to_date                   # => #<Date: 2016-07-22 ((2457592j,0s,0n),+0s,2299161j)>

d.prev_year                     # => #<Date: 2015-07-22 ((2457226j,0s,0n),+0s,2299161j)>
d.prev_year(2)                  # => #<Date: 2014-07-22 ((2456861j,0s,0n),+0s,2299161j)>

d.next_month(6)                 # => #<Date: 2017-01-22 ((2457776j,0s,0n),+0s,2299161j)>
d.prev_month(3)                 # => #<Date: 2016-04-22 ((2457501j,0s,0n),+0s,2299161j)>
d << 6                          # => #<Date: 2016-01-22 ((2457410j,0s,0n),+0s,2299161j)>
d >> 3                          # => #<Date: 2016-10-22 ((2457684j,0s,0n),+0s,2299161j)>

d.next_day(7)                   # => #<Date: 2016-07-29 ((2457599j,0s,0n),+0s,2299161j)>
d.prev_day(14)                  # => #<Date: 2016-07-08 ((2457578j,0s,0n),+0s,2299161j)>
d + 7                           # => #<Date: 2016-07-29 ((2457599j,0s,0n),+0s,2299161j)>
d - 14                          # => #<Date: 2016-07-08 ((2457578j,0s,0n),+0s,2299161j)>

d.prev_year.to_time             # => 2015-07-22 00:00:00 -0400
``````

One point to bear in mind when converting time objects to date objects is that in the process, we're throwing away any time-of-day information.

If we want to keep this part of our time objects intact, for instance if we want to calculate a year ago to the hour, minute, and second, we can switch to converting to DateTime instead of to Date. All the same operations are still available, but the time of day and time zone is preserved.

``````require "date"

t = Time.new(2016, 7, 22, 13, 4)       # => 2016-07-22 13:04:00 -0400
d = t.to_datetime
# => #<DateTime: 2016-07-22T13:04:00-04:00 ((2457592j,61440s,0n),-14400s,2299161j)>
d.prev_year.to_time             # => 2015-07-22 13:04:00 -0400
``````

The date calculations available on `Date` and `DateTime` still aren't quite as rich as those available from ActiveSupport. But they take care of most basic date calculation needs, without having to resort to painful and inaccurate arithmetic using only numbers of seconds.

So now you know what to do if you ever need to perform date calculations outside of a Rails project. Today's episode has also provided a glimpse into why Ruby has Date and DateTime classes that are separate from the Time class. `Time` is just a lightweight wrapper over a count of seconds. Whereas the `Date` and `DateTime` objects encapsulate a true concept of a calendar date, including all the fiddly details of different calendar systems, leap years, and everything else that goes into getting date calculations right.

Happy hacking!