Elixir does have native functionality that handles dates and times, but since it is a more recent addition to Elixir, so it's still in the process of maturing. Elixir 1.3 added standard data structures to support dates and times and Elixir 1.5 added some functionality to manipulate dates and times. I expect that this functionality will be slowly improved upon as Elixir evolves.
The Calendar
module contains the rules that are used to manipulate dates, but there's only one set of rules in Calendar.ISO
, which represents the Gregorian calendar. If you wanted your software to use the Julian calendar, for example, you'd either have to find a library that has these calendar rules or implement the rules yourself.
Dates
A Date
type represents a date on the calendar: there is no time component. It holds a day, month, and year as well as a reference to the calendar that it applies to. A Date
, like many Elixir data types, is actually a struct that is packaged with functions that operate on that struct.
We can use the ~D
sigil to create a date literal that represents October 11, 2018 on the Gregorian calendar.
iex> date = ~D[2018-10-11]
~D[2018-10-11]
iex> date = ~D[2018-20-11]
** (ArgumentError) cannot parse "2018-20-11" as date, reason: :invalid_date
(elixir) lib/calendar/date.ex:312: Date.from_iso8601!/2
(elixir) expanding macro: Kernel.sigil_D/2
iex:8: (file)
As you can see, we get an ArgumentError when we attempt to create a date literal using an invalid date. When IEx sees a Date
struct, is displays it with ~D[]
. I wonder if there's some sort of function in the Date
struct module that tells IEx how to display dates. That would certainly be useful for displaying the values of any data types I create in the future.
Besides using a sigil, we can also use Date.new/3
to create a new data
iex> date = Date.new(2018, 10, 11)
{:ok, ~D[2018-10-11]}
Notice that the above example actually returns a tuple indicating that the date was constructed successfully along with the data structure representing the date.
Let's see what happens when we try constructing an invalid date. I predict that I'll either get an :error
or a tuple containing that same atom.
iex> date = Date.new(2018, 10, 33)
{:error, :invalid_date}
Yep, since there's no October 33, 2018, we got a tuple with :error
and an :invalid_date
atom indicating what the error is.
So let's do pattern matching to extract the date structure from the tuple.
iex> {:ok, date} = Date.new(2018, 10, 11)
{:ok, ~D[2018-10-11]}
iex> date
~D[2018-10-11]
We can compare dates to each other using comparison operators
iex> easter = ~D[2018-04-01]
~D[2018-04-01]
iex> christmas = ~D[2018-12-25]
~D[2018-12-25]
iex> today = ~D[2018-12-25]
~D[2018-12-25]
iex> today == easter
false
iex> today == christmas
true
iex> easter < christmas
true
iex> christmas > easter
true
iex> christmas < easter
false
Inspecting the Date
While learning about dates, I learned that we can also use the inspect
command, a command I wasn't previously familiar with, to look at the internals of the structure.
iex> inspect date
"~D[2018-10-11]"
iex> inspect date, structs: false
"%{__struct__: Date, calendar: Calendar.ISO, day: 11, month: 10, year: 2018}"
It's interesting to see what's in a data structure, and I think that inspect
will prove useful in the future.
Useful Date Functions
The Date
module has a variety of functions, which I may or may not cover in detail in a future post. I'm going to go over a few of them here.
The Date.add/2
function allows us to add days to a particular date.
iex> Date.add(~D[2018-12-11], 10)
~D[2018-12-21]
iex> Date.add(~D[2018-12-11], -10)
~D[2018-12-01]
iex> Date.add(~D[2018-12-11], -20)
~D[2018-11-21]
iex> Date.add(~D[2018-12-11], 42)
~D[2019-01-22]
The Date.day_of_week/1
returns the day of the week associated with a date. 1
is Monday, 2
is a Tuesday, and so forth until we get to 7
, which is a Sunday.
iex> Date.day_of_week(~D[2018-12-11])
2
iex> Date.day_of_week(~D[2018-12-09])
7
iex> Date.day_of_week(~D[2018-12-14])
5
The Date.days_in_month/1
function returns the number of days in the month that the date is in.
iex> Date.days_in_month(~D[2018-12-11])
31
iex> Date.days_in_month(~D[2018-11-11])
30
iex> Date.days_in_month(~D[2019-02-11])
28
Date ranges
We can construct a date range using Date.range/2
and passing two dates
iex> Date.range(~D[2018-04-01], ~D[2018-12-25])
#DateRange<~D[2018-04-01], ~D[2018-12-25]>
It's also possible to have a descending date range.
iex> Date.range(~D[2018-12-25], ~D[2018-04-01])
#DateRange<~D[2018-12-25], ~D[2018-04-01]>
Date ranges appear to be enumerables, which I just love. This means that we can use any function in the Enum
module on date ranges. For example, we can use Enum.count/1
to count the number of days in the date range
iex> Enum.count(Date.range(~D[2018-04-01], ~D[2018-12-25]))
269
iex> Enum.count(Date.range(~D[2018-04-01], ~D[2018-04-01]))
1
iex> Enum.count(Date.range(~D[2018-04-01], ~D[2018-03-25]))
8
It looks like it will also count descending date ranges without a problem. It looks like date ranges work just like integer ranges.
As with other enumerables, you can use the in
keyword to see if a date is within the range, just like with integer ranges
iex> ~D[2018-05-06] in Date.range(~D[2018-04-01], ~D[2018-12-25])
true
iex> ~D[2019-01-16] in Date.range(~D[2018-04-01], ~D[2018-12-25])
false
Like with integer ranges, I can look at all the individual dates in a date range by enumerating the date range with Enum.to_list/1
.
iex> date_range = Date.range(~D[2018-11-28], ~D[2018-12-25])
#DateRange<~D[2018-11-28], ~D[2018-12-25]>
iex> Enum.to_list(date_range)
[~D[2018-11-28], ~D[2018-11-29], ~D[2018-11-30], ~D[2018-12-01], ~D[2018-12-02],
~D[2018-12-03], ~D[2018-12-04], ~D[2018-12-05], ~D[2018-12-06], ~D[2018-12-07],
~D[2018-12-08], ~D[2018-12-09], ~D[2018-12-10], ~D[2018-12-11], ~D[2018-12-12],
~D[2018-12-13], ~D[2018-12-14], ~D[2018-12-15], ~D[2018-12-16], ~D[2018-12-17],
~D[2018-12-18], ~D[2018-12-19], ~D[2018-12-20], ~D[2018-12-21], ~D[2018-12-22],
~D[2018-12-23], ~D[2018-12-24], ~D[2018-12-25]]
I can even get a list containing all Mondays within the date range
iex> Enum.filter(date_range, fn date -> Date.day_of_week(date) == 1 end)
[~D[2018-12-03], ~D[2018-12-10], ~D[2018-12-17], ~D[2018-12-24]]
That was a lot of fun!
Times
The Time
struct contains an hour, minute, second, and fractions of a second. The fraction of a second is stored as a tuple containing a number of microseconds and the number of significant digits.
Like dates, there is a sigil for defining time literals.
#This is the time 20:31:14 with 234 microseconds added on
iex> ~T[20:31:14.234]
~T[20:31:14.234]
#Invalid time
iex> ~T[35:100:31]
** (ArgumentError) cannot parse "35:100:31" as time, reason: :invalid_format
(elixir) lib/calendar/time.ex:268: Time.from_iso8601!/2
(elixir) expanding macro: Kernel.sigil_T/2
iex:25: (file)
#The seconds are required
iex> ~T[04:32]
** (ArgumentError) cannot parse "04:32" as time, reason: :invalid_format
(elixir) lib/calendar/time.ex:268: Time.from_iso8601!/2
(elixir) expanding macro: Kernel.sigil_T/2
iex:25: (file)
#Valid time without the fractional part
iex> ~T[04:32:00]
~T[04:32:00]
Based on my experiments, the time in the time sigil is always 24-hour time. So 8:31 PM is represented as 20:31. It also appears that a time needs the hour, minute, and second, although the fractional second part appears to be optional.
Like with dates, there is a new
function for creating times as well.
iex> Time.new(11, 55, 31)
{:ok, ~T[11:55:31]}
iex> Time.new(35, 100, 31)
{:error, :invalid_time}
Times can be compared just like dates and the Time
module contains functions for manipulating time structures.
DateTime Structures
There are also DateTime
and NaiveDateTime
structures. The difference between them is that DateTime
is associated with a timezone, whereas NaiveDateTime
is not. I have not yet seen examples of these two structures being used. The documentation for the two modules indicates that these modules are mostly used for conversion purposes, converting to and from various formats.
Third-Party Libraries
Those of you who've dealt with dates, times, calendars, and time zones know that this is a very complex subject matter. Elixir's native date and time functionality may be good enough for many applications, but if you want more sophisticated functionality, the standard third-party library to use seems to be Lau Taarnskov's Calendar library, which implements much more than is available in core Elixir.