Packages
timex
2.0.0
3.7.13
3.7.12
3.7.11
3.7.9
3.7.8
3.7.7
3.7.6
3.7.5
3.7.3
3.7.2
3.7.1
3.7.0
3.6.4
3.6.3
3.6.2
retired
3.6.1
3.6.0
3.5.0
3.4.2
3.4.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.25
retired
3.1.24
3.1.23
3.1.22
3.1.21
3.1.20
3.1.19
3.1.18
3.1.17
3.1.16
3.1.15
3.1.13
3.1.12
3.1.11
3.1.10
3.1.9
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.2.1
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.0.2
1.0.1
1.0.0
1.0.0-rc4
1.0.0-rc3
1.0.0-rc2
1.0.0-rc1
1.0.0-pre
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.2
0.10.1
0.10.0
0.9.0
0.8.0
0.7.1
0.6.0
0.5.0
0.4.8
0.4.7
0.4.6
Timex is a rich, comprehensive Date/Time library for Elixir projects, with full timezone support via the :tzdata package. If you need to manipulate dates, times, datetimes, timestamps, etc., then Timex is for you!
Current section
Files
Jump to
Current section
Files
lib/datetime/datetime.ex
defmodule Timex.DateTime do
@moduledoc """
A type which represents a date and time with timezone information (optional, UTC will
be assumed for date/times with no timezone information provided).
Functions that produce time intervals use UNIX epoch (or simly Epoch) as the
default reference date. Epoch is defined as UTC midnight of January 1, 1970.
Time intervals in this module don't account for leap seconds.
"""
require Record
import Timex.Macros
use Timex.Constants
alias __MODULE__
alias Timex.AmbiguousDateTime
alias Timex.Date
alias Timex.Time
alias Timex.Timezone
alias Timex.TimezoneInfo
alias Timex.AmbiguousTimezoneInfo
alias Timex.Types
alias Timex.Helpers
defstruct day: 1,
month: 1,
year: 0,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
timezone: nil,
calendar: :gregorian
@type t :: %__MODULE__{}
@doc """
Creates a new DateTime struct for today's date, at the beginning of the day
"""
@spec today() :: DateTime.t | {:error, term}
@spec today(Timezone.t | String.t | :utc | :local) :: DateTime.t | {:error, term}
def today, do: Timex.beginning_of_day(now)
def today(%TimezoneInfo{} = tz), do: Timex.beginning_of_day(now(tz))
def today(tz) when is_binary(tz), do: Timex.beginning_of_day(now(tz))
def today(tz) when tz in [:utc, :local], do: Timex.beginning_of_day(now(tz))
def today(_), do: {:error, :invalid_timezone}
@doc """
Get the current date and time.
"""
@spec now() :: DateTime.t
def now do
construct(Helpers.calendar_universal_time(), %TimezoneInfo{})
end
@doc """
Get the current date and time, in a specific timezone.
"""
@spec now(TimezoneInfo.t | String.t | :utc | :local) :: DateTime.t
def now(tz) when is_binary(tz), do: Timezone.convert(now(), tz)
def now(%TimezoneInfo{} = tz), do: Timezone.convert(now(), tz)
def now(:utc), do: now()
def now(:local), do: local()
@doc """
Get representation of the current date and time in seconds or days since Epoch.
"""
@spec now(:secs | :days) :: integer
def now(:seconds), do: to_seconds(now())
def now(:days), do: to_days(now())
def now(:secs) do
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead\n"
now(:seconds)
end
def now(_), do: {:error, :invalid_unit}
@doc """
Get current date and time in the local timezone.
See also `universal/0`.
"""
@spec local() :: DateTime.t
def local do
date = construct(Helpers.calendar_local_time())
tz = Timezone.local(date)
%{date | :timezone => tz}
end
@doc """
Convert a DateTime to the local timezone.
See also `universal/1`.
"""
@spec local(date :: DateTime.t) :: DateTime.t
def local(%DateTime{:timezone => tz} = date) do
case Timezone.local(date) do
^tz -> date
new_zone -> Timezone.convert(date, new_zone)
end
end
def local(_), do: {:error, :badarg}
@doc """
Get the current date and time in UTC.
See also `local/0`. Delegates to `now/0`, since they are identical in behavior
"""
@spec universal() :: DateTime.t
defdelegate universal, to: __MODULE__, as: :now
@doc """
Convert a DateTime to UTC
See also `local/1`.
"""
@spec universal(DateTime.t) :: DateTime.t
def universal(%DateTime{} = date), do: Timezone.convert(date, %TimezoneInfo{})
def universal(_), do: {:error, :badarg}
@doc """
Get a DateTime representing the first moment of the first day of year zero (:calendar module's default reference date).
See also `epoch/0`.
## Examples
iex> use Timex
...> date = %DateTime{year: 0, month: 1, day: 1, timezone: %TimezoneInfo{}}
...> #{__MODULE__}.zero === date
true
"""
@spec zero() :: DateTime.t
def zero, do: construct({0, 1, 1}, {0, 0, 0}, %TimezoneInfo{})
@doc """
Get a DateTime representing the first moment of the UNIX epoch (1970/1/1).
Timex uses the UNIX epoch as it's default reference date, but this can be overridden on a per-function basis where applicable.
See also `zero/0`.
## Examples
iex> use Timex
...> date = %DateTime{year: 1970, month: 1, day: 1, timezone: %TimezoneInfo{}}
...> #{__MODULE__}.epoch === date
true
"""
@spec epoch() :: DateTime.t
def epoch, do: construct({1970, 1, 1}, {0, 0, 0}, %TimezoneInfo{})
@doc """
Time interval since year 0 of Epoch expressed in the specified units.
## Examples
iex> #{__MODULE__}.epoch(:timestamp)
{0,0,0}
iex> #{__MODULE__}.epoch(:seconds)
62167219200
"""
@spec epoch(:timestamp) :: Types.timestamp
@spec epoch(:seconds | :days) :: integer
def epoch(:timestamp), do: to_timestamp(epoch())
def epoch(:seconds), do: to_seconds(epoch(), :zero)
def epoch(:secs) do
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead\n"
epoch(:seconds)
end
def epoch(_), do: {:error, :badarg}
@doc """
Construct a date from an Erlang date or datetime value.
You may specify the date's time zone as the second argument. If the argument
is omitted, UTC time zone is assumed.
When passing {year, month, day} as the first argument, the resulting date
will indicate midnight of that day in the specified timezone (UTC by
default).
NOTE: When using `from` the input value is normalized to prevent invalid
dates from being accidentally introduced. Use `set` with `validate: false`,
or create the %DateTime{} by hand if you do not want normalization.
## Examples
> DateTime.from(:erlang.universaltime) #=> %DateTime{...}
> DateTime.from(:erlang.localtime) #=> %Datetime{...}
> DateTime.from(:erlang.localtime, :local) #=> %DateTime{...}
> DateTime.from({2014,3,16}, "America/Chicago") #=> %DateTime{...}
> DateTime.from(phoenix_datetime_select_params) #=> %DateTime{...}
"""
@spec from(Types.valid_datetime | Types.phoenix_datetime_select_params) :: DateTime.t | AmbiguousDateTime.t | {:error, term}
def from(%DateTime{} = date),
do: date
def from(%Date{year: y, month: m, day: d}),
do: from_erl({y, m, d}, :utc)
def from({y,m,d} = date) when is_date(y,m,d),
do: from_erl(date, :utc)
def from({{y,m,d},{h,mm,s}} = datetime) when is_datetime(y,m,d,h,mm,s),
do: from_erl(datetime, :utc)
def from({{y,m,d},{h,mm,s,ms}} = datetime) when is_datetime(y,m,d,h,mm,s,ms),
do: from_erl(datetime, :utc)
def from({{y,m,d} = date, {h,mm,s} = time,{offset,tz}}) when is_gregorian(y,m,d,h,mm,s,offset,tz),
do: from_erl({date,time}, offset)
def from({{y,m,d} = date, {h,mm,s} = time, %TimezoneInfo{} = tz}) when is_datetime(y,m,d,h,mm,s),
do: from_erl({date,time}, tz)
def from({{y,m,d} = date, {h,mm,s} = time, tz}) when is_datetime(y,m,d,h,mm,s) and is_tz_value(tz),
do: from_erl({date,time}, tz)
def from({{y,m,d} = date, {h,mm,s,ms} = time, %TimezoneInfo{} = tz}) when is_datetime(y,m,d,h,mm,s,ms),
do: from_erl({date,time}, tz)
def from({{y,m,d} = date, {h,mm,s,ms} = time, tz}) when is_datetime(y,m,d,h,mm,s,ms) and is_tz_value(tz),
do: from_erl({date,time}, tz)
def from(map) when is_map(map),
do: from_phoenix_datetime_select(map, :utc)
def from(_),
do: {:error, :invalid_datetime}
@doc """
Like from/1, but allows providing a timezone to assume/convert for the input date/time.
"""
@spec from(Types.valid_datetime | Types.phoenix_datetime_select_params, Types.valid_timezone) :: DateTime.t | AmbiguousDateTime.t | {:error, term}
def from(_, nil),
do: {:error, :invalid_timezone}
# Timex types
def from(%DateTime{:timezone => tz} = date, tz),
do: date
def from(%DateTime{} = date, %TimezoneInfo{} = tz),
do: %{date | :timezone => tz}
def from(%DateTime{} = date, tz) when is_tz_value(tz),
do: %{date | :timezone => Timezone.get(tz, date)}
def from(%Date{year: y, month: m, day: d}, %TimezoneInfo{} = tz),
do: from_erl({y,m,d}, tz)
def from(%Date{year: y, month: m, day: d}, tz) when is_tz_value(tz),
do: from_erl({y,m,d}, tz)
# Erlang date tuple
def from({y,m,d} = date, tz) when is_date(y,m,d),
do: from_erl(date, tz)
# Legacy: :timestamp is deprecated
def from({mega,sec,micro} = timestamp, :timestamp) when is_date_timestamp(mega,sec,micro) do
IO.write :stderr, "warning: DateTime.from(_, :timestamp) is deprecated, use from_timestamp/1 or from_timestamp/2 instead\n"
from_timestamp(timestamp, :epoch)
end
# Datetime tuples with timezone
def from({{y,m,d} = date, {h,mm,s} = time, {offset,abbr}}, tz)
when is_gregorian(y,m,d,h,mm,s,offset,abbr),
do: from_erl({date, time}, offset) |> Timezone.convert(tz)
def from({{y,m,d} = date, {h,mm,s} = time, %TimezoneInfo{} = tz}, %TimezoneInfo{} = tznew)
when is_datetime(y,m,d,h,mm,s),
do: from_erl({date, time}, tz) |> Timezone.convert(tznew)
def from({{y,m,d} = date, {h,mm,s} = time, %TimezoneInfo{} = tz}, tznew)
when is_datetime(y,m,d,h,mm,s) and is_tz_value(tznew),
do: from_erl({date, time}, tz) |> Timezone.convert(tznew)
# Erlang datetime tuples
def from({y,m,d} = date, tz) when is_date(y,m,d),
do: from_erl(date, tz)
def from({{y,m,d}, {h,mm,s}} = datetime, tz) when is_datetime(y,m,d,h,mm,s),
do: from_erl(datetime, tz)
def from({{y,m,d}, {h,mm,s,ms}} = datetime, tz) when is_datetime(y,m,d,h,mm,s,ms),
do: from_erl(datetime, tz)
# Phoenix datetime select
def from(%{"year" => _, "month" => _, "day" => _, "hour" => _, "min" => _} = dt, tz) do
from_phoenix_datetime_select(dt, tz)
end
# Not a datetime, try deprecated from(value, unit, :epoch)
def from(value, unit) when is_atom(unit), do: from(value, unit, :epoch)
@doc """
Converts from an Ecto DateTime tuple to a DateTime struct
"""
@spec from_ecto(Types.date | Types.datetime) :: DateTime.t | {:error, term}
def from_ecto(date, tz \\ %TimezoneInfo{}), do: from_erl(date, tz)
@doc """
Converts from an Erlang datetime tuple (including those with milliseconds) to a DateTime struct
"""
@spec from_erl(Types.date | Types.datetime, Types.valid_timezone | nil) :: DateTime.t | {:error, term}
def from_erl(date),
do: from_erl(date, %TimezoneInfo{})
def from_erl({y,m,d} = date, %TimezoneInfo{} = tz) when is_date(y,m,d),
do: from_erl({date, {0,0,0}}, tz)
def from_erl({y,m,d} = date, tz) when is_date(y,m,d) and is_tz_value(tz),
do: from_erl({date, {0,0,0}}, Timezone.get(tz, {date,{0,0,0}}))
def from_erl({_,_,_}, tz),
do: {:error, {:invalid_timezone, tz}}
def from_erl({{y,m,d} = date, {h,mm,s}}, %TimezoneInfo{} = tz) when is_datetime(y,m,d,h,mm,s),
do: from_erl({date, {h,mm,s,0}}, tz)
def from_erl({{y,m,d} = date, {h,mm,s}}, %AmbiguousTimezoneInfo{} = tz) when is_datetime(y,m,d,h,mm,s),
do: from_erl({date, {h,mm,s,0}}, tz)
def from_erl({{y,m,d} = date, {h,mm,s} = time}, tz) when is_datetime(y,m,d,h,mm,s) and is_tz_value(tz),
do: from_erl({date, {h,mm,s,0}}, Timezone.get(tz, {date, time}))
def from_erl({{_,_,_},{_,_,_}}, tz),
do: {:error, {:invalid_timezone, tz}}
def from_erl({{y,m,d}, {h,min,s,ms}}, %TimezoneInfo{} = tz) when is_datetime(y,m,d,h,min,s,ms) do
case :calendar.valid_date({y,m,d}) do
true ->
Timezone.resolve(tz.full_name, {{y,m,d},{h,min,s,ms}})
false ->
{:error, :invalid_date}
end
end
def from_erl({{y,m,d}, {h,min,s,ms}} = datetime, %AmbiguousTimezoneInfo{} = tz) when is_datetime(y,m,d,h,min,s,ms) do
case :calendar.valid_date({y,m,d}) do
true ->
before_dt = %{construct(datetime) | :timezone => tz.before}
after_dt = %{construct(datetime) | :timezone => tz.after}
%AmbiguousDateTime{:before => before_dt, :after => after_dt}
false ->
{:error, :invalid_date}
end
end
def from_erl({{y,m,d} = date, {h,min,s,ms}}, tz) when is_datetime(y,m,d,h,min,s,ms) and is_tz_value(tz) do
case :calendar.valid_date({y,m,d}) do
true ->
case Timezone.get(tz, {date, {h,min,s}}) do
{:error, _} = err ->
err
%TimezoneInfo{} = timezone ->
%DateTime{year: y, month: m, day: d, hour: h, minute: min, second: s, millisecond: ms, timezone: timezone}
%AmbiguousTimezoneInfo{:before => before_tz, :after => after_tz} ->
before_dt = %DateTime{year: y, month: m, day: d, hour: h, minute: min, second: s, millisecond: ms, timezone: before_tz}
after_dt = %DateTime{year: y, month: m, day: d, hour: h, minute: min, second: s, millisecond: ms, timezone: after_tz}
%AmbiguousDateTime{:before => before_dt, :after => after_dt}
end
false ->
{:error, :invalid_date}
end
end
def from_erl(_, _) do
{:error, :badarg}
end
@doc """
Converts the value of a Phoenix date/time field to a DateTime struct.
An optional timezone provided as a second parameter sets the timezone of the DateTime.
"""
@spec from_phoenix_datetime_select(Map.t) :: DateTime.t | {:error, atom}
@spec from_phoenix_datetime_select(Map.t, Types.valid_timezone) :: DateTime.t | {:error, atom}
def from_phoenix_datetime_select(dt), do: from_phoenix_datetime_select(dt, :utc)
def from_phoenix_datetime_select(%{"year" => _, "month" => _, "day" => _, "hour" => _, "min" => _} = dt, tz) do
validated = Enum.reduce(dt, %{}, fn
_, :error -> :error
{key, value}, acc ->
case Integer.parse(value) do
{v, _} -> Map.put(acc, key, v)
:error -> :error
end
end)
case {validated, tz} do
{%{"year" => y, "month" => m, "day" => d, "hour" => h, "min" => mm}, tz} ->
from_erl({{y,m,d},{h,mm,0}}, tz)
{:error, _} ->
{:error, :invalid}
end
end
def from_phoenix_datetime_select(_, _) do
{:error, :badarg}
end
@doc """
Converts an Erlang timestamp to a DateTime struct representing that moment in time
"""
@spec from_timestamp(Types.timestamp) :: DateTime.t
@spec from_timestamp(Types.timestamp, :epoch | :zero) :: DateTime.t | {:error, atom}
def from_timestamp(timestamp, ref \\ :epoch)
def from_timestamp({mega, sec, micro}, ref)
when is_date_timestamp(mega,sec,micro) and ref in [:epoch, :zero],
do: from_microseconds((mega * @million + sec) * @million + micro, ref)
def from_timestamp(_, _) do
{:error, :badarg}
end
@doc """
Converts an integer value representing days since the reference date (:epoch or :zero)
to a DateTime struct representing that moment in time
"""
@spec from_days(non_neg_integer) :: DateTime.t | {:error, atom}
@spec from_days(non_neg_integer, :epoch | :zero) :: DateTime.t | {:error, atom}
def from_days(days, ref \\ :epoch)
def from_days(days, :epoch) when is_positive_number(days) do
construct(:calendar.gregorian_days_to_date(trunc(days) + to_days(epoch(), :zero)), {0,0,0}, %TimezoneInfo{})
end
def from_days(days, :zero) when is_positive_number(days) do
construct(:calendar.gregorian_days_to_date(trunc(days)), {0,0,0}, %TimezoneInfo{})
end
def from_days(_, _) do
{:error, :badarg}
end
@doc """
Converts an integer value representing seconds since the reference date (:epoch or :zero)
to a DateTime struct representing that moment in time
"""
@spec from_seconds(non_neg_integer) :: DateTime.t :: {:error, atom}
@spec from_seconds(non_neg_integer, :epoch | :zero) :: DateTime.t :: {:error, atom}
def from_seconds(s, ref \\ :epoch)
def from_seconds(s, :epoch) when is_positive_number(s) do
construct(:calendar.gregorian_seconds_to_datetime(trunc(s) + epoch(:seconds)), %TimezoneInfo{})
end
def from_seconds(s, :zero) when is_positive_number(s) do
construct(:calendar.gregorian_seconds_to_datetime(trunc(s)), %TimezoneInfo{})
end
def from_seconds(_, _) do
{:error, :badarg}
end
@doc """
Converts an integer representing milliseconds since the reference date
(:epoch or :zero) to a DateTime struct representing that moment in time
"""
@spec from_milliseconds(non_neg_integer) :: DateTime.t :: {:error, atom}
@spec from_milliseconds(non_neg_integer, :epoch | :zero) :: DateTime.t :: {:error, atom}
def from_milliseconds(ms, type \\ :epoch)
def from_milliseconds(ms, type) when is_positive_number(ms) and type in [:epoch, :zero],
do: from_timestamp(Time.from(ms, :milliseconds), type)
def from_milliseconds(_, _) do
{:error, :badarg}
end
@doc """
Converts an integer representing microseconds since the reference date
(:epoch or :zero) to a DateTime struct representing that moment in time
"""
@spec from_microseconds(non_neg_integer) :: DateTime.t :: {:error, atom}
@spec from_microseconds(non_neg_integer, :epoch | :zero) :: DateTime.t :: {:error, atom}
def from_microseconds(us, type \\ :epoch)
def from_microseconds(us, :epoch) when is_positive_number(us) do
construct(Helpers.calendar_gregorian_microseconds_to_datetime(trunc(us), epoch(:seconds)), %TimezoneInfo{})
end
def from_microseconds(us, :zero) when is_positive_number(us) do
construct(Helpers.calendar_gregorian_microseconds_to_datetime(trunc(us), 0), %TimezoneInfo{})
end
def from_microseconds(_, _) do
{:error, :badarg}
end
@doc """
Construct a date from a time interval since Epoch or year 0.
UTC time zone is assumed. This assumption can be modified by setting desired
time zone using set/3 after the date is constructed.
## Examples
> DateTime.from(13, :seconds)
> DateTime.from(13, :days, :zero)
> DateTime.from(Time.now, :timestamp)
"""
defdeprecated from(timestamp, :timestamp, ref) when is_atom(ref), "use from_timestamp/1 or from_timestamp/2 instead",
do: from_timestamp(timestamp, ref)
defdeprecated from(us, :us, ref) when is_atom(ref), "use from_microseconds/1 or from_microseconds/2 instead",
do: from_microseconds(us, ref)
defdeprecated from(ms, :msecs, ref) when is_atom(ref), "use from_milliseconds/1 or from_milliseconds/2 instead",
do: from_milliseconds(ms, ref)
defdeprecated from(s, :secs, ref) when is_atom(ref), "use from_seconds/1 or from_seconds/2 instead",
do: from_seconds(s, ref)
defdeprecated from(days, :days, ref) when is_atom(ref), "use from_days/1 or from_days/2 instead",
do: from_days(days, ref)
@doc """
Convert a date to a timestamp value consumable by the Time module.
See also `diff/2` if you want to specify an arbitrary reference date.
## Examples
iex> #{__MODULE__}.epoch |> #{__MODULE__}.to_timestamp
{0,0,0}
"""
@spec to_timestamp(DateTime.t) :: Types.timestamp | {:error, atom}
@spec to_timestamp(DateTime.t, :epoch | :zero) :: Types.timestamp | {:error, atom}
def to_timestamp(date, reference \\ :epoch)
def to_timestamp(%DateTime{:millisecond => ms} = date, :epoch) do
case ok!(to_seconds(date)) do
{:error, _} = err -> err
{:ok, seconds} ->
{ div(seconds, @million), rem(seconds, @million), ms * 1_000}
end
end
def to_timestamp(%DateTime{:millisecond => ms} = date, :zero) do
case ok!(to_seconds(date, :zero)) do
{:error, _} = err -> err
{:ok, seconds} ->
{ div(seconds, @million), rem(seconds, @million), ms * 1_000}
end
end
def to_timestamp(_, _), do: {:error, :badarg}
defdelegate to_secs(date), to: __MODULE__, as: :to_seconds
defdelegate to_secs(date, ref), to: __MODULE__, as: :to_seconds
defdelegate to_secs(date, ref, options), to: __MODULE__, as: :to_seconds
@doc """
Convert a date to an integer number of seconds since Epoch or year 0.
With `to_seconds/3`, you can also specify an option `utc: false | true`,
which controls whether the DateTime is converted to UTC prior to calculating
the number of seconds from the reference date. By default, UTC conversion is
enabled.
See also `diff/2` if you want to specify an arbitrary reference date.
## Examples
iex> Timex.datetime({{1999, 1, 2}, {12,13,14}}) |> #{__MODULE__}.to_seconds
915279194
"""
@spec to_seconds(DateTime.t) :: integer | {:error, atom}
@spec to_seconds(DateTime.t, :epoch | :zero) :: integer | {:error, atom}
@spec to_seconds(DateTime.t, :epoch | :zero, [utc: false | true]) :: integer | {:error, atom}
def to_seconds(date, reference \\ :epoch, options \\ [utc: true])
def to_seconds(%DateTime{} = date, :epoch, utc: true) do
case to_seconds(date, :zero, utc: true) do
{:error, _} = err -> err
secs -> secs - epoch(:seconds)
end
end
def to_seconds(%DateTime{} = date, :zero, utc: true) do
converted = Timezone.convert(date, :utc)
utc_to_secs(converted)
#offset = Timex.Timezone.diff(date, %TimezoneInfo{})
#secs = utc_to_secs(date)
#case secs do
#{:error, _} = err -> err
#_ ->
#secs + (60 * offset)
#end
end
def to_seconds(%DateTime{} = date, :epoch, utc: false) do
case to_seconds(date, :zero, utc: false) do
{:error, _} = err -> err
secs -> secs - epoch(:seconds)
end
end
def to_seconds(%DateTime{} = date, :zero, utc: false),
do: utc_to_secs(date)
def to_seconds(_, _, _), do: {:error, :badarg}
defp utc_to_secs(%DateTime{:year => y, :month => m, :day => d, :hour => h, :minute => min, :second => s}) do
case :calendar.valid_date({y,m,d}) do
false -> {:error, :invalid_date}
true ->
:calendar.datetime_to_gregorian_seconds({{y, m, d}, {h, min, s}})
end
end
@doc """
Convert the date to an integer number of days since Epoch or year 0.
See also `diff/2` if you want to specify an arbitray reference date.
## Examples
iex> Timex.datetime({1970, 1, 15}) |> #{__MODULE__}.to_days
14
"""
@spec to_days(DateTime.t) :: integer | {:error, atom}
@spec to_days(DateTime.t, :epoch | :zero) :: integer | {:error, atom}
def to_days(date, reference \\ :epoch)
def to_days(date, :epoch) do
case to_days(date, :zero) do
{:error, _} = err -> err
days -> days - to_days(epoch(), :zero)
end
end
def to_days(%DateTime{:year => y, :month => m, :day => d}, :zero) do
case :calendar.valid_date({y, m, d}) do
false -> {:error, :invalid_date}
true -> :calendar.date_to_gregorian_days({y, m, d})
end
end
def to_days(_, _), do: {:error, :badarg}
@doc """
Return a new DateTime 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.
"""
@spec set(DateTime.t, list({atom(), term})) :: DateTime.t | {:error, term}
def set(%DateTime{} = date, options) do
validate? = case options |> List.keyfind(:validate, 0, true) do
{:validate, bool} -> bool
_ -> true
end
Enum.reduce(options, date, fn
_option, {:error, _} = err ->
err
option, result ->
case option do
{:validate, _} -> result
{:datetime, {{y, m, d}, {h, min, sec}}} ->
if validate? do
%{result |
:year => Timex.normalize(:year, y),
:month => Timex.normalize(:month, m),
:day => Timex.normalize(:day, {y,m,d}),
:hour => Timex.normalize(:hour, h),
:minute => Timex.normalize(:minute, min),
:second => Timex.normalize(:second, sec)
}
else
%{result | :year => y, :month => m, :day => d, :hour => h, :minute => min, :second => sec}
end
{:date, {y, m, d}} ->
if validate? do
{yn,mn,dn} = Timex.normalize(:date, {y,m,d})
%{result | :year => yn, :month => mn, :day => dn}
else
%{result | :year => y, :month => m, :day => d}
end
{:time, {h, m, s}} ->
if validate? do
%{result | :hour => Timex.normalize(:hour, h), :minute => Timex.normalize(:minute, m), :second => Timex.normalize(:second, s)}
else
%{result | :hour => h, :minute => m, :second => s}
end
{:day, d} ->
if validate? do
%{result | :day => Timex.normalize(:day, {result.year, result.month, d})}
else
%{result | :day => d}
end
{:timezone, tz} ->
tz = case tz do
%TimezoneInfo{} -> tz
_ -> Timezone.get(tz, result)
end
if validate? do
%{result | :timezone => Timex.normalize(:timezone, tz)}
else
%{result | :timezone => tz}
end
{name, val} when name in [:year, :month, :hour, :minute, :second, :millisecond] ->
if validate? do
Map.put(result, name, Timex.normalize(name, val))
else
Map.put(result, name, val)
end
{option_name, _} ->
{:error, {:bad_option, option_name}}
end
end)
end
@doc """
Compare two dates returning one of the following values:
* `-1` -- the first date comes before the second one
* `0` -- both arguments represent the same date when coalesced to the same timezone.
* `1` -- the first date comes after the second one
See the docs for Timex.compare/2 or Timex.compare/3 for more details.
"""
@spec compare(DateTime.t, DateTime.t | :epoch | :zero | :distant_past | :distant_future) :: -1 | 0 | 1 | {:error, term}
@spec compare(DateTime.t, DateTime.t, :years | :months | :weeks | :days | :hours | :minutes | :seconds | :timestamp) :: -1 | 0 | 1 | {:error, term}
def compare(date, :epoch), do: compare(date, epoch())
def compare(date, :zero), do: compare(date, zero())
def compare(_, :distant_past), do: +1
def compare(_, :distant_future), do: -1
def compare(date, date), do: 0
def compare(a, b), do: compare(a, b, :seconds)
def compare(this, other, granularity)
when granularity in [:years, :months, :weeks, :days, :hours, :minutes, :seconds, :timestamp] do
case {ok!(to_seconds(this)), ok!(to_seconds(other))} do
{{:error, _} = err, _} ->
err
{_, {:error, _} = err} ->
err
{{:ok, this_secs}, {:ok, other_secs}} ->
case ok!(diff(this, other, granularity)) do
{:error, _} = err ->
err
{:ok, delta} ->
occurs_after? = cond do
other_secs < this_secs -> true
:else -> false
end
cond do
delta == 0 -> 0
delta > 0 && occurs_after? -> 1
:else -> -1
end
end
end
end
def compare(this, other, :mins) do
IO.write :stderr, "warning: :mins is a deprecated unit name, use :minutes instead\n"
compare(this, other, :minutes)
end
def compare(this, other, :secs) do
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead\n"
compare(this, other, :seconds)
end
def compare(_, _, granularity), do: {:error, {:invalid_granularity, granularity}}
@doc """
Calculate time interval between two dates. If the second date comes after the
first one in time, return value will be positive; and negative otherwise.
See docs for Timex.diff/3 for more details.
"""
@spec diff(DateTime.t, DateTime.t, :timestamp) :: Types.timestamp | {:error, term}
@spec diff(DateTime.t, DateTime.t, :seconds | :minutes | :hours | :days | :weeks | :calendar_weeks | :months | :years) :: integer | {:error, term}
def diff(this, other, :secs) do
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead\n"
diff(this, other, :seconds)
end
def diff(this, other, :mins) do
IO.write :stderr, "warning: :mins is a deprecated unit name, use :minutes instead\n"
diff(this, other, :minutes)
end
def diff(this, other, type) do
case {ok!(to_seconds(this, :zero)), ok!(to_seconds(other, :zero))} do
{{:error, _} = err, _} ->
err
{_, {:error, _} = err} ->
err
{{:ok, this_secs}, {:ok, other_secs}} ->
diff_secs = this_secs - other_secs
cond do
diff_secs == 0 -> 0
diff_secs > 0 -> do_diff(other, this, type)
diff_secs < 0 -> do_diff(this, other, type)
end
end
end
defp do_diff(this, other, :timestamp) do
case ok!(do_diff(this, other, :seconds)) do
{:error, _} = err -> err
{:ok, seconds} ->
case ok!(Time.from(seconds, :seconds)) do
{:error, _} = err -> err
{:ok, timestamp} -> timestamp
end
end
end
defp do_diff(this, other, :seconds) do
case {ok!(to_seconds(this, :zero)), ok!(to_seconds(other, :zero))} do
{{:error, _} = err, _} -> err
{_, {:error, _} = err} -> err
{{:ok, this_secs}, {:ok, other_secs}} ->
other_secs - this_secs
end
end
defp do_diff(this, other, :minutes) do
case ok!(do_diff(this, other, :seconds)) do
{:ok, seconds} -> div(seconds, 60)
{:error, _} = err -> err
end
end
defp do_diff(this, other, :hours) do
case ok!(do_diff(this, other, :minutes)) do
{:ok, minutes} -> div(minutes, 60)
{:error, _} = err -> err
end
end
defp do_diff(this, other, :days) do
case {ok!(to_days(this, :zero)), ok!(to_days(other, :zero))} do
{{:error, _} = err, _} -> err
{_, {:error, _} = err} -> err
{{:ok, this_days}, {:ok, other_days}} ->
other_days - this_days
end
end
defp do_diff(this, other, :weeks) do
case ok!(do_diff(this, other, :days)) do
{:error, _} = err -> err
{:ok, days} ->
weeks = div(days, 7)
extra_days = rem(days, 7)
actual_weeks = (if extra_days == 0, do: weeks, else: weeks + 1)
cond do
actual_weeks == 1 && extra_days < 7 -> 0
:else -> actual_weeks
end
end
end
defp do_diff(this, other, :calendar_weeks) do
case {ok!(Timex.beginning_of_week(this)), ok!(Timex.end_of_week(other))} do
{{:error, _} = err, _} -> err
{_, {:error, _} = err} -> err
{{:ok, start}, {:ok, ending}} ->
case ok!(do_diff(start, ending, :days)) do
{:error, _} = err -> err
{:ok, days} ->
weeks = div(days, 7)
extra_days = rem(days, 7)
actual_weeks = (if extra_days == 0, do: weeks, else: weeks + 1)
cond do
actual_weeks == 1 && extra_days < 7 -> 0
:else -> actual_weeks
end
end
end
end
defp do_diff(this, other, :months) do
result = case compare(this, other, :seconds) do
0 -> 0
-1 -> {this, other}
1 -> {other, this}
{:error, _} = err -> err
end
case result do
{:error, _} = err -> err
0 -> 0
{%DateTime{year: eyear, month: emonth, day: eday}, %DateTime{year: lyear, month: lmonth, day: lday}} ->
x = cond do
lday >= eday -> 0
:else -> -1
end
y = lyear - eyear
z = lmonth - emonth
x+y*12+z
end
end
defp do_diff(this, other, :years) do
case ok!(do_diff(this, other, :months)) do
{:error, _} = err -> err
{:ok, months} ->
years = div(months, 12)
years
end
end
defp do_diff(_, _, unit), do: {:error, {:invalid_unit, unit}}
@doc """
Shifts the given DateTime based on a series of options.
See docs for Timex.shift/2 for details.
"""
@spec shift(DateTime.t, list({atom(), term})) :: DateTime.t | {:error, term}
def shift(%DateTime{} = datetime, shifts) when is_list(shifts) do
apply_shifts(datetime, shifts)
end
defp apply_shifts(datetime, []),
do: datetime
defp apply_shifts(datetime, [{:timestamp, {0,0,0}} | rest]),
do: apply_shifts(datetime, rest)
defp apply_shifts(datetime, [{:timestamp, {_,_,_} = timestamp} | rest]) do
total_milliseconds = Time.to_milliseconds(timestamp)
seconds = div(total_milliseconds, 1_000)
milliseconds = rem(total_milliseconds, 1_000)
datetime
|> shift_by(seconds, :seconds)
|> shift_by(milliseconds, :milliseconds)
|> apply_shifts(rest)
end
defp apply_shifts(datetime, [{unit, 0} | rest]) when is_atom(unit),
do: apply_shifts(datetime, rest)
defp apply_shifts(datetime, [{:secs, value} | rest]) do
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead\n"
apply_shifts(datetime, [{:seconds, value} | rest])
end
defp apply_shifts(datetime, [{:mins, value} | rest]) do
IO.write :stderr, "warning: :mins is a deprecated unit name, use :minutes instead\n"
apply_shifts(datetime, [{:minutes, value} | rest])
end
defp apply_shifts(datetime, [{unit, value} | rest]) when is_atom(unit) and is_integer(value) do
shifted = shift_by(datetime, value, unit)
apply_shifts(shifted, rest)
end
defp apply_shifts({:error, _} = err, _),
do: err
# Primary constructor for DateTime objects
defp construct({{_, _, _} = date, {_, _, _} = time}), do: construct(date, time, %TimezoneInfo{})
defp construct({{_, _, _} = date, {_, _, _, _} = time}), do: construct(date, time, %TimezoneInfo{})
defp construct({_,_,_} = date, {_,_,_} = time, nil), do: construct(date, time, %TimezoneInfo{})
defp construct({_,_,_} = date, {_,_,_,_} = time, nil), do: construct(date, time, %TimezoneInfo{})
defp construct(date, {h, min, sec}, %TimezoneInfo{} = tz), do: construct(date, {h, min, sec, 0}, tz)
defp construct({_,_,_}=date, {_,_,_,_}=time, %TimezoneInfo{} = tz) do
{y,m,d} = Timex.normalize(:date, date)
{h,min,sec,ms} = Timex.normalize(:time, time)
%DateTime{
year: y, month: m, day: d,
hour: h, minute: min, second: sec,
millisecond: ms,
timezone: tz
}
end
defp construct({_,_,_}=date, {_,_,_,_}=time, {_, name}) do
{y,m,d} = Timex.normalize(:date, date)
{h,min,sec,ms} = Timex.normalize(:time, time)
dt = %DateTime{
year: y, month: m, day: d,
hour: h, minute: min, second: sec,
millisecond: ms
}
%{dt | :timezone => Timezone.get(name, dt)}
end
defp construct(date, {h, min, sec}, tz), do: construct(date, {h, min, sec, 0}, tz)
defp construct({date, time}, tz), do: construct(date, time, tz)
defp shift_by(%AmbiguousDateTime{:before => before_dt, :after => after_dt}, value, unit) do
# Since we're presumably in the middle of a shifting operation, rather than failing because
# we're crossing an ambiguous time period, process both the before and after DateTime individually,
# and if both return AmbiguousDateTimes, choose the AmbiguousDateTime from :after to return,
# if one returns a valid DateTime, return that instead, since the shift has resolved the ambiguity.
# if one returns an error, but the other does not, return the non-errored one.
case {shift_by(before_dt, value, unit), shift_by(after_dt, value, unit)} do
# other could be an error too, but that's fine
{{:error, _}, other} ->
other
{other, {:error, _}} ->
other
# We'll always use :after when choosing between two ambiguous datetimes
{%AmbiguousDateTime{}, %AmbiguousDateTime{} = new_after} ->
new_after
# The shift resolved the ambiguity!
{%AmbiguousDateTime{}, %DateTime{} = resolved} ->
resolved
{%DateTime{}, %AmbiguousDateTime{} = resolved} ->
resolved
end
end
defp shift_by(%DateTime{:year => y, :millisecond => ms} = datetime, value, :years) do
shifted = %{datetime | :year => y + value}
# If a plain shift of the year fails, then it likely falls on a leap day,
# so add one day in seconds to the original date, and shift that instead
# if that still fails, an error tuple will be returned
case :calendar.valid_date({shifted.year,shifted.month,shifted.day}) do
false ->
plus_one_day = :calendar.datetime_to_gregorian_seconds({
{datetime.year,datetime.month,datetime.day},
{datetime.hour,datetime.minute,datetime.second}
}) + (60 * 60 * 24)
{{y,m,d},{h,mm,s}} = :calendar.gregorian_seconds_to_datetime(plus_one_day)
resolve_timezone_info(from_erl({{y+value,m,d}, {h,mm,s,ms}}, datetime.timezone))
true ->
resolve_timezone_info(shifted)
end
end
defp shift_by(%DateTime{:year => year, :month => month, :millisecond => ms} = datetime, value, :months) do
m = month + value
shifted = cond do
value == 12 -> %{datetime | :year => year + 1}
value == -12 -> %{datetime | :year => year - 1}
m == 0 -> %{datetime | :year => year - 1, :month => 12}
m > 12 -> %{datetime | :year => year + div(m, 12), :month => rem(m, 12)}
m < 0 -> %{datetime | :year => year + div(m, 12), :month => 12 + rem(m, 12)}
:else -> %{datetime | :month => m}
end
# Again, if the shift fails, it likely falls on a leap day,
# so add one day in seconds to the original date, and shift that instead
# if that still fails, an error tuple will be returned
case :calendar.valid_date({shifted.year,shifted.month,shifted.day}) do
false ->
plus_one_day = :calendar.datetime_to_gregorian_seconds({
{datetime.year,datetime.month,datetime.day},
{datetime.hour,datetime.minute,datetime.second}
}) + (60 * 60 * 24)
{{y,m,d},{h,mm,s}} = :calendar.gregorian_seconds_to_datetime(plus_one_day)
case m + value do
months when months > 12 ->
resolve_timezone_info(from_erl({{y + div(months, 12), rem(months, 12), d}, {h,mm,s,ms}}))
months ->
resolve_timezone_info(from_erl({{y,months,d}, {h,mm,s,ms}}))
end
true ->
resolve_timezone_info(shifted)
end
end
defp shift_by(%DateTime{millisecond: ms} = datetime, value, units) do
secs_from_zero = :calendar.datetime_to_gregorian_seconds({
{datetime.year,datetime.month,datetime.day},
{datetime.hour,datetime.minute,datetime.second}
})
shift_by = case units do
:milliseconds -> div(value + ms, 1_000)
:seconds -> value
:minutes -> value * 60
:hours -> value * 60 * 60
:days -> value * 60 * 60 * 24
:weeks -> value * 60 * 60 * 24 * 7
_ ->
{:error, {:unknown_shift_unit, units}}
end
case shift_by do
{:error, _} = err -> err
0 when units in [:milliseconds] ->
total_ms = rem(value + ms, 1_000)
%{datetime | :millisecond => total_ms}
0 ->
datetime
_ ->
new_secs_from_zero = secs_from_zero + shift_by
cond do
new_secs_from_zero <= 0 ->
{:error, :shift_to_invalid_date}
:else ->
{{y,m,d}=date,{h,mm,s}} = :calendar.gregorian_seconds_to_datetime(new_secs_from_zero)
Timezone.resolve(datetime.timezone.full_name, {date, {h,mm,s,ms}})
end
end
end
defp resolve_timezone_info(%DateTime{:timezone => %TimezoneInfo{:full_name => tzname}} = datetime) do
Timezone.resolve(tzname, {
{datetime.year, datetime.month, datetime.day},
{datetime.hour, datetime.minute, datetime.second, datetime.millisecond}})
end
end