The Calendar library has functionality for comparing dates and datetimes. The Calendar.Date module has a function for getting the difference between two dates. You can for instance pass date tuples or Calendar structs to the Calendar.Date.diff/2 funciton. This will return the difference in days between the two dates.

iex> Calendar.Date.diff({2016, 3, 10}, {2016, 3, 4})
6
iex> Calendar.Date.diff({2016, 3, 10}, %Calendar.Date{day: 4, month: 3, year: 2016})
6
> Calendar.Date.diff({2016, 3, 4}, {2016, 3, 10})
-6
> Calendar.Date.diff({2016, 3, 10}, {2016, 3, 10})
0

Ecto is another library that has structs for dates and datetimes.

To get an Ecto date struct you can use the Ecto.Date.from_erl function like this:

> Ecto.Date.from_erl({2016, 3, 10})
#Ecto.Date<2016-03-10>

What if you could use the same function in the calendar library to compare Ecto.Date structs? You can!

The Calecto library contains a protocol implementation that makes this possible. It looks like this:

defimpl Calendar.ContainsDate, for: Ecto.Date do
  def date_struct(data) do
    Calendar.Date.from_erl!({data.year, data.month, data.day})
  end
end

This means that if you have added Calecto to your Elixir project, you can pass Ecto.Date structs to the same function:

> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.diff({2016, 3, 4})
6

In the above example an Ecto.Date struct is compared to a tuple. Whatever implements the ContainsDate protocol can be used as parameters.

We can also compare two Ecto.Date structs:

> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.diff(Ecto.Date.from_erl({2016, 2, 1}))
38

All the other functions

But that is not all. diff/2 is just one of the functions in the Calendar.Date module. Any place where a date is expected you can use Ecto.Date structs, Calendar.Date structs or tuples as you please. In addition to that you can implement the Calendar.ContainsDate for your own custom date types.

# what day of the week it is:
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.day_of_week_name
"Thursday"
# ISO 8601 basic format
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.Format.iso8601_basic
"20160310"
# Format as ordinal date
Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.Format.ordinal
"2016-070"
# Is it on a tuesday?
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.tuesday?
false
# Get an infinite Stream of all Dates after the Ecto.Date
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.days_after
#Function<32.16851754/2 in Stream.unfold/2>
# Add 10 days to the Date
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.add!(10)
%Calendar.Date{day: 20, month: 3, year: 2016}
# Subtract a day
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Date.subtract!(1)
%Calendar.Date{day: 9, month: 3, year: 2016}
# Format with strftime
> Ecto.Date.from_erl({2016, 3, 10}) |> Calendar.Strftime.strftime!("The year is %Y")
"The year is 2016"

Those are just a few examples of the available functionality.

Comparing Ecto.DateTime structs

Because Ecto.DateTime structs have datetimes, but no timezone information they are equivalent to Calendars NaiveDateTime structs.

Like Calendar.Date functions take arguments that implement the Calendar.ContainsDate protocol, Calendar.NaiveDateTime functions take arguments that implement the Calendar.ContainsNaiveDateTime protocol. Out of the box Calendar has implemented these protocols for Calendar.NaiveDateTime structs and erlang style tuples.

Calecto implements the protocol for Ecto.DateTime structs. This means we can use the Calendar.NaiveDateTime.diff/2 function to compare Ecto.DateTime structs too.

> ecto_dt1 = {{2016, 1, 1}, {0, 0, 0}} |> Ecto.DateTime.from_erl
#Ecto.DateTime<2016-01-01 00:00:00>
> ecto_dt2 = {{2016, 1, 1}, {0, 10, 3}} |> Ecto.DateTime.from_erl
#Ecto.DateTime<2016-01-01 00:10:03>
> Calendar.NaiveDateTime.diff(ecto_dt1, ecto_dt2)
{:ok, -603, 0, :before}

The difference between the two structs is 603 seconds and 0 microseconds. You can pattern match on the fourth tuple element to get :before :after or :same_time. Alternatively if you do not want to know how big the difference is, but just want to know if the first datetime is before the second one or not, there is a function for that:

> Calendar.NaiveDateTime.before?(ecto_dt1, ecto_dt2)
true
> ecto_dt1 |> Calendar.NaiveDateTime.after?(ecto_dt2)
false

And as with the Date module you can also use Ecto.DateTimes in the other functions in the NaiveDateTime module:

# Add 10 seconds
> ecto_dt1 |> Calendar.NaiveDateTime.add(10)
{:ok, %Calendar.NaiveDateTime{day: 1, hour: 0, min: 0, month: 1, sec: 10, usec: 0, year: 2016}}
# Subtract 3600 seconds
> ecto_dt1 |> Calendar.NaiveDateTime.subtract!(3600)
%Calendar.NaiveDateTime{day: 31, hour: 23, min: 0, month: 12, sec: 0, usec: 0, year: 2015}
# Format as ISO 8601 basic
> ecto_dt1 |> Calendar.NaiveDateTime.Format.iso8601_basic
"20160101T000000"

Comparing just the date part of a datetime

Because Ecto.Datetime contains a date - the ContainsDate protocol is also implemented for Ecto.Datetime. As with a simple date we can also use the functions in the Calendar.Date module with an Ecto.DateTime as an argument.

What if you have two datetimes and you want to know if they are on the same date - not if they are on the exact same time, just if they are on the same date. There is not a special function for that made for date-times. Instead we can simply use the same function as before: Date.diff/2. When passing datetimes, the protocols simply discards the time and just uses the date part of the date-time:

> Calendar.Date.diff(ecto_dt1, ecto_dt2)
0
> Calendar.Date.same_date?(ecto_dt1, ecto_dt2)
true
> ecto_dt1 |> Calendar.Date.day_of_week_name
"Friday"
> ecto_dt1 |> Calendar.Date.week_number
{2015, 53}

How to get all of this goodness

Just add Calendar (and Calecto if you use Ecto) to your mix file. Then you can start using the features of Calendar without changing anything else in your existing code.

The whole way

So far we have seen how the protocols effortlessly allow using date and time types from Ecto with Calendar modules.

If you go a bit further and use Calecto types e.g. Calecto.DateTimeUTC instead of Ecto.DateTime, you get more functionality, convenience and protection from bugs.

Protocols

With Elixir protocols it is possible to build functionality once in one library and then use it with data from other libraries without changing a single line of code in either! All you have to do is implement the necessary protocols. In this example those libraries are Calendar and Ecto. And Calecto provides the protocol implementation.

Even within one library the protocols provide a clean way of making one function usable with different data-types.