Working with DateTime
A breakdown of the Timex.Date API
NOTE: As a rule, Timex validates dates/times are valid within certain constraints, but it makes no guarantees around user-provided input. Some functions in the Timex API have high complexity relative to the input values, and if you are not careful to sanitize input to these functions, an attacker can exploit this to lock processes calling these functions, and potentially DoS your system. The only general exception to this rule is with datetime parsing, which is very strict about what values are considered valid. Some format strings do allow unbounded values to be provided however, such as the {s-epoch} format token. As such, it is recommended that you take care to specify format strings which are as restrictive as possible.
Get the current date
# In UTC
> Date.now
# In the local timezone
> Date.local
# In an arbitrary timezone
> Date.now("America/Chicago")
# Get the number of seconds since Epoch
> Date.now(:secs)
Convert to a timezone
# To local time
> date |> Date.local
# To universal time
> date |> Date.universal
# To an arbitrary timezone
> date |> Timezone.convert("America/Chicago")
Addition/subtraction
Adding
# Date.add takes a DateTime and a timestamp ({megasecs, secs, microsecs})
> date = Date.now
%DateTime{calendar: :gregorian, day: 24, hour: 14, minute: 27, month: 6,
ms: 821, second: 52,
timezone: %TimezoneInfo{...}, year: 2015}
> date |> Date.add(Time.to_timestamp(8, :days))
%DateTime{calendar: :gregorian, day: 2, hour: 14, minute: 27, month: 7,
ms: 0, second: 52,
timezone: %TimezoneInfo{...}, year: 2015}
Subtracting
# Date.subtract, like add, takes a DateTime and a timestamp
> date = Date.now
%DateTime{calendar: :gregorian, day: 24, hour: 14, minute: 27, month: 6,
ms: 821, second: 52,
timezone: %TimezoneInfo{...}, year: 2015}
> date |> Date.subtract(Time.to_timestamp(8, :days))
%DateTime{calendar: :gregorian, day: 16, hour: 14, minute: 27, month: 6,
ms: 0, second: 52,
timezone: %TimezoneInfo{...}, year: 2015}
Shifting through time
Shifts rely on a specification of how to shift the DateTime, the valid values for these shift specs are: :timestamp
, :secs
, :mins
, :hours
, :days
, :weeks
, :months
, :years
.
NOTE: Currently :months
is not supported in complex shifts (i.e. [months: 2, days: 3]
).
# Shifts are a more flexible way of moving a DateTime through time and
# range from simple shifts...
> date = Date.now
> date |> Date.shift(days: 5)
# To more complex shifts...
> date |> Date.shift([days: 5, hours: 3, mins: 2])
Get the century for a given DateTime
# Gets the current century
> Date.century
21
# Gets the century of the provided DateTime
> Date.from({{1437, 3, 5}, {12, 0, 0}}) |> Date.century
15
Comparisons
Compare two dates returning one of the following values:
-1
-- the first date comes before the second one0
-- both arguments represent the same date when coalesced to the same timezone.1
-- the first date comes after the second one
You can optionality specify a granularity using:
- :years
- :months
- :weeks
- :days
- :hours
- :mins
- :secs
- :timestamp
The dates will be compared with the corresponding accuracy. The default granularity is :secs.
# Comparing two DateTimes
> date1 = Date.now
> date2 = Date.now |> Date.add(Time.to_timestamp(10, :mins))
> Date.compare(date1, date2)
-1
> Date.compare(date1, date1)
0
> Date.compare(date2, date1)
1
# Comparing a DateTime against reference points
> date = Date.now
> Date.compare(date, :epoch)
1
> date = Date.from({{1969, 1, 1}, {12, 0, 0}})
> Date.compare(date, :epoch)
-1
> Date.compare(date, :zero)
1
> Date.compare(date, :distant_past)
1 # This is always 1
> Date.compare(date, :distant_future)
-1 # This is always -1
Equality
> date1 = Date.now
> date2 = Date.now |> Date.add(Time.to_timestamp(10, :mins))
> Date.equal?(date1, date2)
false
> Date.equal?(date1, date1)
true
Ordinal Conversions
Get day of the year for a given DateTime
> Date.now |> Date.day
175
Determine the ordinal day of the week for a given DateTime
> Date.now |> Date.weekday
3
Get the name of the day of week based on ordinal number
> Date.day_name(1)
"Monday"
# Or the abbreviated name
> Date.day_shortname(1)
"Mon"
Get the ordinal weekday for the given weekday name
> Date.day_to_num("Monday")
1
> Date.day_to_num("Mon")
1
> Date.day_to_num(:mon)
1
Get the number of days in the month for a DateTime or year/month combo
> Date.now |> Date.days_in_month
30
> Date.days_in_month(2015, 6)
30
Diffs
Get the difference between two dates in the given units
Valid units are: * :timestamp * :years * :months * :weeks * :days * :hours * :mins * :secs
> date1 = Date.from({{1970, 1, 1}, {0, 0, 0}})
> date2 = Date.from({{1970, 2, 4}, {12, 5, 5}})
> Date.diff(date1, date2, :timestamp)
{2, 981105, 0}
> Date.diff(date1, date2, :secs)
2981105
> Date.diff(date1, date2, :weeks)
4
Epoch
Get the Epoch in various forms
> Date.epoch
%DateTime{}
> Date.epoch(:timestamp)
{0,0,0}
> Date.epoch(:secs)
62167219200
Constructing DateTimes
Convert to DateTime from various representations
# From date tuple (time is in UTC and set to midnight)
> Date.from({2015, 1, 1})
# From date tuple w/ timezone
> Date.from({2015, 1, 1}, "America/Chicago")
# From date tuple using local timezone
> Date.from({2015, 1, 1}, :local)
# You can do all of the above with datetime tuples, e.x:
> Date.from({{2015, 1, 1}, {4, 0, 0}}, "America/Chicago")
# There is a special datetime tuple containing milliseconds which is also supported
> Date.from({{2015, 1, 1}, {4, 0, 0, 132}}, "America/Chicago")
# Convert from timestamp (assumes timestamp from Epoch)
> Date.from(Time.now, :timestamp)
# Convert from timestamp relative to year zero
> Date.from(Time.now, :timestamp, :zero)
# Convert from microseconds, seconds or days from Epoch or year zero
> Date.from(1000, :us)
> Date.from(1000, :secs)
> Date.from(1000, :days)
> Date.from(1000, :days, :zero)
Convert to DateTime from ISO triplet (year, weeknumber, weekday)
> Date.from_iso_triplet({2015, 3, 5})
%DateTime{calendar: :gregorian, day: 17, hour: 0, minute: 0, month: 1,
ms: 0, second: 0,
timezone: %TimezoneInfo{...}, year: 2015}
Leap Years
Determine if the given year is a leap year
> Date.is_leap?(2012)
true
> Date.is_leap?(2015)
false
Validation
Determine if the given DateTime represents a valid point in time
> Date.now |> Date.is_valid?
true
> %DateTime{year: -1} |> Date.is_valid?
false
ISO Conversions
Convert from ISO day of the year to DateTime
> 175 |> Date.from_iso_day
%DateTime{calendar: :gregorian, day: 25, hour: 0, minute: 0, month: 6,
ms: 0, second: 0,
timezone: %TimezoneInfo{...}, year: 2015}
# You can also shift a date to the given ISO day, which will preserve the timezone (unless a transition is required) and time information.
> date = Date.now
> Date.from_iso_day(120, date)
Get the ISO triplet (year, week number, week day) from a DateTime
> Date.now |> Date.iso_triplet
{2015, 26, 3}
Get the ISO week number from a DateTime
> Date.now |> Date.iso_week
{2015, 26}
Get the name of the month corresponding to it's ordinal number
> Date.month_name(3)
"March"
> Date.month_shortname(3)
"Mar"
Convert a month name to it's ordinal number
> Date.month_to_num("March")
3
> Date.month_to_num("Mar")
3
> Date.month_to_num(:mar)
3
Normalization
Take unvalidated input, normalize it to ensure all the components are clamped to valid values, and convert to a DateTime
> {{-1,3,5}, {26,60,60}} |> Date.normalize
%DateTime{calendar: :gregorian, day: 5, hour: 23, minute: 59, month: 3,
ms: 0, second: 59,
timezone: %TimezoneInfo{...}, year: 0}
Manipulation
Set components of a DateTime manually
Returns a new date with the specified fields replaced by new values.
Values are automatically validated and clamped to good values by default. If you wish to skip validation, perhaps for performance reasons, pass validate: false
.
Values are applied in order, so if you pass [datetime: dt, date: d]
, the date value from date
will override datetime
's date value.
# Set components of the DateTime, `date` allows you to set all date components easily
> Date.now |> Date.set(date: {1,1,1})
> Date.now |> Date.set(hour: 0)
> Date.now |> Date.set([date: {1,1,1}, hour: 30])
# `datetime` is like `date`, but includes time components
# order matters though, in this case `date` will override the date component of `datetime`
> Date.now |> Date.set([datetime: {{1,1,1}, {0,0,0}}, date: {2,2,2}])
# You can disable validation, but be careful
> Date.now |> Date.set([minute: 74, validate: false])
Timezones
Get the Timezone for a moment in time
# For the current time
> Date.timezone("America/Chicago")
# For a specific time
> Date.epoch |> Date.timezone("Europe/Cophenhagen")
Relative Conversions
Convert a DateTime to number of days since Epoch/Year Zero
> Date.to_days(Date.now) # From Epoch
> Date.to_days(Date.now, :zero)
Convert a DateTime to number of seconds since Epoch/Year Zero
> Date.to_secs(Date.now) # From Epoch
> Date.to_secs(Date.now, :zero)
Convert a DateTime to a timestamp relative to Epoch/Year Zero
> Date.to_timestamp(Date.now) # From Epoch
> Date.to_timestamp(Date.now, :zero)