1413 lines
46 KiB
Elixir
1413 lines
46 KiB
Elixir
defmodule Calendar do
|
|
@moduledoc """
|
|
This module defines the responsibilities for working with
|
|
calendars, dates, times and datetimes in Elixir.
|
|
|
|
Currently it defines types and the minimal implementation
|
|
for a calendar behaviour in Elixir. The goal of the Calendar
|
|
features in Elixir is to provide a base for interoperability
|
|
instead of full-featured datetime API.
|
|
|
|
For the actual date, time and datetime structures, see `Date`,
|
|
`Time`, `NaiveDateTime` and `DateTime`.
|
|
|
|
Note the year, month, day, etc designations are overspecified
|
|
(i.e. an integer instead of 1..12 for months) because different
|
|
calendars may have a different number of days per month, months per year and so on.
|
|
"""
|
|
|
|
@type year :: integer
|
|
@type month :: integer
|
|
@type day :: integer
|
|
@type hour :: 0..23
|
|
@type minute :: 0..59
|
|
|
|
@typedoc "From 0 to 60 to account for leap seconds"
|
|
@type second :: 0..60
|
|
|
|
@typedoc """
|
|
Microseconds with stored precision.
|
|
|
|
The precision represents the number of digits
|
|
that must be used when representing the microseconds
|
|
to external format. If the precision is 0, it means
|
|
microseconds must be skipped.
|
|
"""
|
|
@type microsecond :: {0..999_999, 0..6}
|
|
|
|
@typedoc "A calendar implementation"
|
|
@type calendar :: module
|
|
|
|
@typedoc "The time zone ID according to the IANA tz database (e.g. Europe/Zurich)"
|
|
@type time_zone :: String.t
|
|
|
|
@typedoc "The time zone abbreviation (e.g. CET or CEST or BST etc.)"
|
|
@type zone_abbr :: String.t
|
|
|
|
@typedoc "The time zone UTC offset in seconds"
|
|
@type utc_offset :: integer
|
|
|
|
@typedoc "The time zone standard offset in seconds (not zero in summer times)"
|
|
@type std_offset :: integer
|
|
|
|
@doc """
|
|
Builds a new date from proleptic year, month and day of month.
|
|
"""
|
|
@callback date(year, month, day) :: {:ok, Date.t} | {:error, atom}
|
|
|
|
@doc """
|
|
Returns the last day of the month for the given year-month pair.
|
|
"""
|
|
@callback last_day_of_month(year, month) :: day
|
|
|
|
@doc """
|
|
Returns true if the given year is a leap year.
|
|
|
|
A leap year is a year of a longer length than normal. The exact meaning
|
|
is up to the calendar. A calendar must return `false` if it does not support
|
|
the concept of leap years.
|
|
"""
|
|
@callback leap_year?(year) :: boolean
|
|
|
|
@doc """
|
|
Converts the given structure into a string according to the calendar.
|
|
"""
|
|
@callback to_string(structure :: Date.t | DateTime.t | NaiveDateTime.t) :: String.t
|
|
end
|
|
|
|
defmodule Date do
|
|
@moduledoc """
|
|
A Date struct and functions.
|
|
|
|
The Date struct contains the fields year, month, day and calendar.
|
|
New dates can be built with the `new/3` function or using the `~D`
|
|
sigil:
|
|
|
|
iex> ~D[2000-01-01]
|
|
~D[2000-01-01]
|
|
|
|
Both `new/3` and sigil return a struct where the date fields can
|
|
be accessed directly:
|
|
|
|
iex> date = ~D[2000-01-01]
|
|
iex> date.year
|
|
2000
|
|
iex> date.month
|
|
1
|
|
|
|
Developers should avoid creating the Date struct directly and
|
|
instead rely on the functions provided by this module as well as
|
|
the ones in 3rd party calendar libraries.
|
|
"""
|
|
|
|
@enforce_keys [:year, :month, :day]
|
|
defstruct [:year, :month, :day, calendar: Calendar.ISO]
|
|
|
|
@type t :: %__MODULE__{year: Calendar.year, month: Calendar.month,
|
|
day: Calendar.day, calendar: Calendar.calendar}
|
|
|
|
@doc """
|
|
Returns the current date in UTC.
|
|
|
|
## Examples
|
|
|
|
iex> date = Date.utc_today()
|
|
iex> date.year >= 2016
|
|
true
|
|
|
|
"""
|
|
@spec utc_today() :: Date.t
|
|
def utc_today() do
|
|
{:ok, {year, month, day}, _, _} = Calendar.ISO.from_unix(:os.system_time, :native)
|
|
%Date{year: year, month: month, day: day}
|
|
end
|
|
|
|
@doc """
|
|
Builds a new ISO date.
|
|
|
|
Expects all values to be integers. Returns `{:ok, date}` if each
|
|
entry fits its appropriate range, returns `{:error, reason}` otherwise.
|
|
|
|
## Examples
|
|
|
|
iex> Date.new(2000, 1, 1)
|
|
{:ok, ~D[2000-01-01]}
|
|
iex> Date.new(2000, 13, 1)
|
|
{:error, :invalid_date}
|
|
iex> Date.new(2000, 2, 29)
|
|
{:ok, ~D[2000-02-29]}
|
|
|
|
iex> Date.new(2000, 2, 30)
|
|
{:error, :invalid_date}
|
|
iex> Date.new(2001, 2, 29)
|
|
{:error, :invalid_date}
|
|
|
|
"""
|
|
@spec new(Calendar.year, Calendar.month, Calendar.day) :: {:ok, Date.t} | {:error, atom}
|
|
def new(year, month, day) do
|
|
Calendar.ISO.date(year, month, day)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given date to a string according to its calendar.
|
|
|
|
### Examples
|
|
|
|
iex> Date.to_string(~D[2000-02-28])
|
|
"2000-02-28"
|
|
|
|
"""
|
|
@spec to_string(Date.t) :: String.t
|
|
def to_string(%Date{calendar: calendar} = date) do
|
|
calendar.to_string(date)
|
|
end
|
|
|
|
@doc """
|
|
Parses the extended "Date and time of day" format described by ISO8601:2004.
|
|
|
|
Timezone offset may be included in the string but they will be
|
|
simply discarded as such information is not included in naive date
|
|
times.
|
|
|
|
Time representations with reduced accuracy are not supported.
|
|
|
|
## Examples
|
|
|
|
iex> Date.from_iso8601("2015-01-23")
|
|
{:ok, ~D[2015-01-23]}
|
|
|
|
iex> Date.from_iso8601("2015:01:23")
|
|
{:error, :invalid_format}
|
|
iex> Date.from_iso8601("2015-01-32")
|
|
{:error, :invalid_date}
|
|
|
|
"""
|
|
@spec from_iso8601(String.t) :: {:ok, Date.t} | {:error, atom}
|
|
def from_iso8601(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes>>) do
|
|
with {year, ""} <- Integer.parse(year),
|
|
{month, ""} <- Integer.parse(month),
|
|
{day, ""} <- Integer.parse(day) do
|
|
new(year, month, day)
|
|
else
|
|
_ -> {:error, :invalid_format}
|
|
end
|
|
end
|
|
|
|
def from_iso8601(<<_::binary>>) do
|
|
{:error, :invalid_format}
|
|
end
|
|
|
|
@doc """
|
|
Parses the extended "Date and time of day" format described by ISO8601:2004.
|
|
|
|
Raises if the format is invalid.
|
|
|
|
## Examples
|
|
|
|
iex> Date.from_iso8601!("2015-01-23")
|
|
~D[2015-01-23]
|
|
iex> Date.from_iso8601!("2015:01:23")
|
|
** (ArgumentError) cannot parse "2015:01:23" as date, reason: :invalid_format
|
|
"""
|
|
@spec from_iso8601!(String.t) :: Date.t | no_return
|
|
def from_iso8601!(string) do
|
|
case from_iso8601(string) do
|
|
{:ok, value} ->
|
|
value
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot parse #{inspect string} as date, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given date time to ISO8601.
|
|
|
|
Only supports converting date times which are in the ISO calendar,
|
|
attempting to convert date times from other calendars will raise.
|
|
|
|
### Examples
|
|
|
|
iex> Date.to_iso8601(~D[2000-02-28])
|
|
"2000-02-28"
|
|
|
|
"""
|
|
@spec to_iso8601(Date.t) :: String.t
|
|
def to_iso8601(%Date{calendar: Calendar.ISO} = date) do
|
|
Calendar.ISO.to_iso8601(date)
|
|
end
|
|
|
|
@doc """
|
|
Converts a `Date` struct to an Erlang date tuple.
|
|
|
|
Only supports converting dates which are in the ISO calendar,
|
|
attempting to convert dates from other calendars will raise.
|
|
|
|
## Examples
|
|
|
|
iex> Date.to_erl(~D[2000-01-01])
|
|
{2000, 1, 1}
|
|
|
|
"""
|
|
@spec to_erl(Date.t) :: :calendar.date()
|
|
def to_erl(%Date{calendar: Calendar.ISO, year: year, month: month, day: day}) do
|
|
{year, month, day}
|
|
end
|
|
|
|
@doc """
|
|
Converts an Erlang date tuple to a `Date` struct.
|
|
|
|
Attempting to convert an invalid ISO calendar date will produce an error tuple.
|
|
|
|
## Examples
|
|
|
|
iex> Date.from_erl({2000, 1, 1})
|
|
{:ok, ~D[2000-01-01]}
|
|
iex> Date.from_erl({2000, 13, 1})
|
|
{:error, :invalid_date}
|
|
"""
|
|
@spec from_erl(:calendar.date()) :: {:ok, Date.t} | {:error, atom}
|
|
def from_erl({year, month, day}) do
|
|
new(year, month, day)
|
|
end
|
|
|
|
@doc """
|
|
Converts an Erlang date tuple but raises for invalid dates.
|
|
|
|
## Examples
|
|
|
|
iex> Date.from_erl!({2000, 1, 1})
|
|
~D[2000-01-01]
|
|
iex> Date.from_erl!({2000, 13, 1})
|
|
** (ArgumentError) cannot convert {2000, 13, 1} to date, reason: :invalid_date
|
|
"""
|
|
@spec from_erl!(:calendar.date()) :: Date.t | no_return
|
|
def from_erl!(tuple) do
|
|
case from_erl(tuple) do
|
|
{:ok, value} ->
|
|
value
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot convert #{inspect tuple} to date, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
defimpl String.Chars do
|
|
def to_string(%Date{calendar: calendar} = date) do
|
|
calendar.to_string(date)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect do
|
|
def inspect(%Date{calendar: Calendar.ISO} = date, _) do
|
|
"~D[" <> Calendar.ISO.to_string(date) <> "]"
|
|
end
|
|
|
|
def inspect(date, opts) do
|
|
Inspect.Any.inspect(date, opts)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Time do
|
|
@moduledoc """
|
|
A Time struct and functions.
|
|
|
|
The Time struct contains the fields hour, minute, second and microseconds.
|
|
New times can be built with the `new/4` function or using the `~T`
|
|
sigil:
|
|
|
|
iex> ~T[23:00:07.001]
|
|
~T[23:00:07.001]
|
|
|
|
Both `new/4` and sigil return a struct where the time fields can
|
|
be accessed directly:
|
|
|
|
iex> time = ~T[23:00:07.001]
|
|
iex> time.hour
|
|
23
|
|
iex> time.microsecond
|
|
{1000, 3}
|
|
|
|
Developers should avoid creating the Time struct directly and
|
|
instead rely on the functions provided by this module as well as
|
|
the ones in 3rd party calendar libraries.
|
|
"""
|
|
|
|
@enforce_keys [:hour, :minute, :second]
|
|
defstruct [:hour, :minute, :second, microsecond: {0, 0}]
|
|
|
|
@type t :: %__MODULE__{hour: Calendar.hour, minute: Calendar.minute,
|
|
second: Calendar.second, microsecond: Calendar.microsecond}
|
|
|
|
@doc """
|
|
Returns the current time in UTC.
|
|
|
|
## Examples
|
|
|
|
iex> time = Time.utc_now()
|
|
iex> time.hour >= 0
|
|
true
|
|
|
|
"""
|
|
@spec utc_now() :: Time.t
|
|
def utc_now() do
|
|
{:ok, _, {hour, minute, second}, microsecond} = Calendar.ISO.from_unix(:os.system_time, :native)
|
|
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond}
|
|
end
|
|
|
|
@doc """
|
|
Builds a new time.
|
|
|
|
Expects all values to be integers. Returns `{:ok, time}` if each
|
|
entry fits its appropriate range, returns `{:error, reason}` otherwise.
|
|
|
|
Note a time may have 60 seconds in case of leap seconds.
|
|
|
|
## Examples
|
|
|
|
iex> Time.new(0, 0, 0, 0)
|
|
{:ok, ~T[00:00:00.000000]}
|
|
iex> Time.new(23, 59, 59, 999_999)
|
|
{:ok, ~T[23:59:59.999999]}
|
|
iex> Time.new(23, 59, 60, 999_999)
|
|
{:ok, ~T[23:59:60.999999]}
|
|
|
|
# Time with microseconds and their precision
|
|
iex> Time.new(23, 59, 60, {10_000, 2})
|
|
{:ok, ~T[23:59:60.01]}
|
|
|
|
iex> Time.new(24, 59, 59, 999_999)
|
|
{:error, :invalid_time}
|
|
iex> Time.new(23, 60, 59, 999_999)
|
|
{:error, :invalid_time}
|
|
iex> Time.new(23, 59, 61, 999_999)
|
|
{:error, :invalid_time}
|
|
iex> Time.new(23, 59, 59, 1_000_000)
|
|
{:error, :invalid_time}
|
|
|
|
"""
|
|
@spec new(Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond) ::
|
|
{:ok, Time.t} | {:error, atom}
|
|
def new(hour, minute, second, microsecond \\ {0, 0})
|
|
|
|
def new(hour, minute, second, microsecond) when is_integer(microsecond) do
|
|
new(hour, minute, second, {microsecond, 6})
|
|
end
|
|
|
|
def new(hour, minute, second, {microsecond, precision})
|
|
when is_integer(hour) and is_integer(minute) and is_integer(second) and
|
|
is_integer(microsecond) and is_integer(precision) do
|
|
if hour in 0..23 and minute in 0..59 and second in 0..60 and
|
|
microsecond in 0..999_999 and precision in 0..6 do
|
|
{:ok, %Time{hour: hour, minute: minute, second: second, microsecond: {microsecond, precision}}}
|
|
else
|
|
{:error, :invalid_time}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given time to a string.
|
|
|
|
### Examples
|
|
|
|
iex> Time.to_string(~T[23:00:00])
|
|
"23:00:00"
|
|
iex> Time.to_string(~T[23:00:00.001])
|
|
"23:00:00.001"
|
|
iex> Time.to_string(~T[23:00:00.123456])
|
|
"23:00:00.123456"
|
|
|
|
"""
|
|
@spec to_string(Time.t) :: String.t
|
|
def to_string(%Time{} = time) do
|
|
Calendar.ISO.to_string(time)
|
|
end
|
|
|
|
@doc """
|
|
Parses the extended "Local time" format described by ISO8601:2004.
|
|
|
|
Timezone offset may be included in the string but they will be
|
|
simply discarded as such information is not included in times.
|
|
|
|
As specified in the standard, the separator "T" may be omitted if
|
|
desired as there is no ambiguity within this function.
|
|
|
|
Time representations with reduced accuracy are not supported.
|
|
|
|
## Examples
|
|
|
|
iex> Time.from_iso8601("23:50:07")
|
|
{:ok, ~T[23:50:07]}
|
|
iex> Time.from_iso8601("23:50:07Z")
|
|
{:ok, ~T[23:50:07]}
|
|
iex> Time.from_iso8601("T23:50:07Z")
|
|
{:ok, ~T[23:50:07]}
|
|
|
|
iex> Time.from_iso8601("23:50:07.0123456")
|
|
{:ok, ~T[23:50:07.012345]}
|
|
iex> Time.from_iso8601("23:50:07.123Z")
|
|
{:ok, ~T[23:50:07.123]}
|
|
|
|
iex> Time.from_iso8601("2015:01:23 23-50-07")
|
|
{:error, :invalid_format}
|
|
iex> Time.from_iso8601("23:50:07A")
|
|
{:error, :invalid_format}
|
|
iex> Time.from_iso8601("23:50:07.")
|
|
{:error, :invalid_format}
|
|
iex> Time.from_iso8601("23:50:61")
|
|
{:error, :invalid_time}
|
|
|
|
"""
|
|
@spec from_iso8601(String.t) :: {:ok, Time.t} | {:error, atom}
|
|
def from_iso8601(<<?T, h, rest::binary>>) when h in ?0..?9 do
|
|
from_iso8601(<<h, rest::binary>>)
|
|
end
|
|
|
|
def from_iso8601(<<hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>) do
|
|
with {hour, ""} <- Integer.parse(hour),
|
|
{min, ""} <- Integer.parse(min),
|
|
{sec, ""} <- Integer.parse(sec),
|
|
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
|
|
{_offset, ""} <- Calendar.ISO.parse_offset(rest) do
|
|
new(hour, min, sec, microsec)
|
|
else
|
|
_ -> {:error, :invalid_format}
|
|
end
|
|
end
|
|
def from_iso8601(<<_::binary>>) do
|
|
{:error, :invalid_format}
|
|
end
|
|
|
|
@doc """
|
|
Parses the extended "Local time" format described by ISO8601:2004.
|
|
|
|
Raises if the format is invalid.
|
|
|
|
## Examples
|
|
|
|
iex> Time.from_iso8601!("23:50:07.123Z")
|
|
~T[23:50:07.123]
|
|
iex> Time.from_iso8601!("2015:01:23 23-50-07")
|
|
** (ArgumentError) cannot parse "2015:01:23 23-50-07" as time, reason: :invalid_format
|
|
"""
|
|
@spec from_iso8601!(String.t) :: Time.t | no_return
|
|
def from_iso8601!(string) do
|
|
case from_iso8601(string) do
|
|
{:ok, value} ->
|
|
value
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot parse #{inspect string} as time, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given time to ISO8601.
|
|
|
|
### Examples
|
|
|
|
iex> Time.to_iso8601(~T[23:00:13])
|
|
"23:00:13"
|
|
|
|
iex> Time.to_iso8601(~T[23:00:13.001])
|
|
"23:00:13.001"
|
|
|
|
"""
|
|
@spec to_iso8601(Time.t) :: String.t
|
|
def to_iso8601(%Time{} = time) do
|
|
Calendar.ISO.to_iso8601(time)
|
|
end
|
|
|
|
@doc """
|
|
Converts a `Time` struct to an Erlang time tuple.
|
|
|
|
WARNING: Loss of precision may occur, as Erlang time tuples
|
|
only contain hours/minutes/seconds.
|
|
|
|
## Examples
|
|
|
|
iex> Time.to_erl(~T[23:30:15.999])
|
|
{23, 30, 15}
|
|
|
|
"""
|
|
@spec to_erl(Time.t) :: :calendar.time()
|
|
def to_erl(%Time{hour: hour, minute: minute, second: second}) do
|
|
{hour, minute, second}
|
|
end
|
|
|
|
@doc """
|
|
Converts an Erlang time tuple to a `Time` struct.
|
|
|
|
## Examples
|
|
|
|
iex> Time.from_erl({23, 30, 15}, {5000, 3})
|
|
{:ok, ~T[23:30:15.005]}
|
|
iex> Time.from_erl({24, 30, 15})
|
|
{:error, :invalid_time}
|
|
"""
|
|
@spec from_erl(:calendar.time(), Calendar.microsecond) :: {:ok, Time.t} | {:error, atom}
|
|
def from_erl({hour, minute, second}, microsecond \\ {0, 0}) do
|
|
new(hour, minute, second, microsecond)
|
|
end
|
|
|
|
@doc """
|
|
Converts an Erlang time tuple to a `Time` struct.
|
|
|
|
## Examples
|
|
|
|
iex> Time.from_erl!({23, 30, 15})
|
|
~T[23:30:15]
|
|
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
|
|
~T[23:30:15.005]
|
|
iex> Time.from_erl!({24, 30, 15})
|
|
** (ArgumentError) cannot convert {24, 30, 15} to time, reason: :invalid_time
|
|
"""
|
|
@spec from_erl!(:calendar.time(), Calendar.microsecond) :: Time.t | no_return
|
|
def from_erl!(tuple, microsecond \\ {0, 0}) do
|
|
case from_erl(tuple, microsecond) do
|
|
{:ok, value} ->
|
|
value
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot convert #{inspect tuple} to time, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
defimpl String.Chars do
|
|
def to_string(time) do
|
|
Calendar.ISO.to_string(time)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect do
|
|
def inspect(time, _) do
|
|
"~T[" <> Calendar.ISO.to_string(time) <> "]"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule NaiveDateTime do
|
|
@moduledoc """
|
|
A NaiveDateTime struct (without a time zone) and functions.
|
|
|
|
The NaiveDateTime struct contains the fields year, month, day, hour,
|
|
minute, second, microsecond and calendar. New naive date times can be
|
|
built with the `new/7` function or using the `~N` sigil:
|
|
|
|
iex> ~N[2000-01-01 23:00:07]
|
|
~N[2000-01-01 23:00:07]
|
|
|
|
Both `new/7` and sigil return a struct where the date fields can
|
|
be accessed directly:
|
|
|
|
iex> naive = ~N[2000-01-01 23:00:07]
|
|
iex> naive.year
|
|
2000
|
|
iex> naive.second
|
|
7
|
|
|
|
The naive bit implies this datetime representation does
|
|
not have a timezone. This means the datetime may not
|
|
actually exist in certain areas in the world even though
|
|
it is valid.
|
|
|
|
For example, when daylight saving changes are applied
|
|
by a region, the clock typically moves forward or backward
|
|
by one hour. This means certain datetimes never occur or
|
|
may occur more than once. Since `NaiveDateTime` is not
|
|
validated against a timezone, such errors would go unnoticed.
|
|
|
|
Developers should avoid creating the NaiveDateTime struct directly
|
|
and instead rely on the functions provided by this module as well
|
|
as the ones in 3rd party calendar libraries.
|
|
"""
|
|
|
|
@enforce_keys [:year, :month, :day, :hour, :minute, :second]
|
|
defstruct [:year, :month, :day, :hour, :minute, :second, microsecond: {0, 0}, calendar: Calendar.ISO]
|
|
|
|
@type t :: %__MODULE__{year: Calendar.year, month: Calendar.month, day: Calendar.day,
|
|
calendar: Calendar.calendar, hour: Calendar.hour, minute: Calendar.minute,
|
|
second: Calendar.second, microsecond: Calendar.microsecond}
|
|
|
|
@unix_epoch :calendar.datetime_to_gregorian_seconds {{1970, 1, 1}, {0, 0, 0}}
|
|
|
|
@doc """
|
|
Returns the current naive date time in UTC.
|
|
|
|
Prefer using `DateTime.utc_now/0` when possible as, opposite
|
|
to `NaiveDateTime`, it will keep the time zone information.
|
|
|
|
## Examples
|
|
|
|
iex> naive_datetime = NaiveDateTime.utc_now()
|
|
iex> naive_datetime.year >= 2016
|
|
true
|
|
|
|
"""
|
|
@spec utc_now() :: NaiveDateTime.t
|
|
def utc_now() do
|
|
:os.system_time |> from_unix!(:native)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given Unix time to NaiveDateTime.
|
|
|
|
The integer can be given in different unit
|
|
according to `System.convert_time_unit/3` and it will
|
|
be converted to microseconds internally.
|
|
|
|
Even though Unix times are always in UTC, the time zone
|
|
is not stored in the naive date time, occuring in loss
|
|
of information. For this reason, in order to convert a
|
|
NaiveDateTime back to Unix time, you first need to call
|
|
`DateTime.from_naive/2` with a timezone. For such reason,
|
|
prefer using `DateTime.from_unix/2` when possible as, opposite
|
|
to `NaiveDateTime`, it will keep the time zone information.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.from_unix(1464096368)
|
|
{:ok, ~N[2016-05-24 13:26:08]}
|
|
|
|
iex> NaiveDateTime.from_unix(1432560368868569, :microsecond)
|
|
{:ok, ~N[2015-05-25 13:26:08.868569]}
|
|
|
|
The unit can also be an integer as in `System.time_unit`:
|
|
|
|
iex> NaiveDateTime.from_unix(1432560368868569, 1024)
|
|
{:ok, %NaiveDateTime{calendar: Calendar.ISO, day: 23, hour: 22, microsecond: {211914, 3},
|
|
minute: 53, month: 1, second: 43, year: 46302}}
|
|
|
|
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
|
|
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
|
|
|
|
iex> NaiveDateTime.from_unix(-12345678910)
|
|
{:ok, ~N[1578-10-13 04:44:50]}
|
|
|
|
When a Unix time before that moment is passed to `from_unix/2`, `:error` will be returned.
|
|
"""
|
|
@spec from_unix(integer, :native | System.time_unit) :: {:ok, NaiveDateTime.t}
|
|
def from_unix(integer, unit \\ :second) when is_integer(integer) do
|
|
case Calendar.ISO.from_unix(integer, unit) do
|
|
{:ok, {year, month, day}, {hour, minute, second}, microsecond} ->
|
|
{:ok, %NaiveDateTime{year: year, month: month, day: day,
|
|
hour: hour, minute: minute, second: second, microsecond: microsecond}}
|
|
{:error, _} = error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given Unix time to NaiveDateTime.
|
|
|
|
The integer can be given in different unit
|
|
according to `System.convert_time_unit/3` and it will
|
|
be converted to microseconds internally.
|
|
|
|
Even though Unix times are always in UTC, the time zone
|
|
is not stored in the naive date time. Prefer using
|
|
`DateTime.from_unix/2` when possible as, opposite
|
|
to `NaiveDateTime`, it will keep the time zone information.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.from_unix!(1464096368)
|
|
~N[2016-05-24 13:26:08]
|
|
|
|
iex> NaiveDateTime.from_unix!(1432560368868569, :microsecond)
|
|
~N[2015-05-25 13:26:08.868569]
|
|
|
|
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
|
|
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
|
|
|
|
iex> NaiveDateTime.from_unix!(-12345678910)
|
|
~N[1578-10-13 04:44:50]
|
|
|
|
When a Unix time before that moment is passed to `from_unix/2`, `:error` will be returned.
|
|
"""
|
|
@spec from_unix!(non_neg_integer, :native | System.time_unit) :: NaiveDateTime.t
|
|
def from_unix!(integer, unit \\ :second) when is_atom(unit) do
|
|
case from_unix(integer, unit) do
|
|
{:ok, datetime} ->
|
|
datetime
|
|
{:error, :invalid_unix_time} ->
|
|
raise ArgumentError, "invalid Unix time #{integer}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Builds a new ISO naive date time.
|
|
|
|
Expects all values to be integers. Returns `{:ok, naive_date_time}`
|
|
if each entry fits its appropriate range, returns `{:error, reason}`
|
|
otherwise.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.new(2000, 1, 1, 0, 0, 0)
|
|
{:ok, ~N[2000-01-01 00:00:00]}
|
|
iex> NaiveDateTime.new(2000, 13, 1, 0, 0, 0)
|
|
{:error, :invalid_date}
|
|
iex> NaiveDateTime.new(2000, 2, 29, 0, 0, 0)
|
|
{:ok, ~N[2000-02-29 00:00:00]}
|
|
iex> NaiveDateTime.new(2000, 2, 30, 0, 0, 0)
|
|
{:error, :invalid_date}
|
|
iex> NaiveDateTime.new(2001, 2, 29, 0, 0, 0)
|
|
{:error, :invalid_date}
|
|
|
|
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 59, {0, 1})
|
|
{:ok, ~N[2000-01-01 23:59:59.0]}
|
|
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 59, 999_999)
|
|
{:ok, ~N[2000-01-01 23:59:59.999999]}
|
|
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 60, 999_999)
|
|
{:ok, ~N[2000-01-01 23:59:60.999999]}
|
|
iex> NaiveDateTime.new(2000, 1, 1, 24, 59, 59, 999_999)
|
|
{:error, :invalid_time}
|
|
iex> NaiveDateTime.new(2000, 1, 1, 23, 60, 59, 999_999)
|
|
{:error, :invalid_time}
|
|
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 61, 999_999)
|
|
{:error, :invalid_time}
|
|
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 59, 1_000_000)
|
|
{:error, :invalid_time}
|
|
|
|
"""
|
|
@spec new(Calendar.year, Calendar.month, Calendar.day,
|
|
Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond) ::
|
|
{:ok, NaiveDateTime.t} | {:error, atom}
|
|
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}) do
|
|
with {:ok, date} <- Calendar.ISO.date(year, month, day),
|
|
{:ok, time} <- Time.new(hour, minute, second, microsecond),
|
|
do: new(date, time)
|
|
end
|
|
|
|
@doc """
|
|
Builds a naive date time from date and time structs.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.new(~D[2010-01-13], ~T[23:00:07.005])
|
|
{:ok, ~N[2010-01-13 23:00:07.005]}
|
|
|
|
"""
|
|
@spec new(Date.t, Time.t) :: {:ok, NaiveDateTime.t}
|
|
def new(date, time)
|
|
|
|
def new(%Date{calendar: calendar, year: year, month: month, day: day},
|
|
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond}) do
|
|
{:ok, %NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
|
|
hour: hour, minute: minute, second: second, microsecond: microsecond}}
|
|
end
|
|
|
|
@doc """
|
|
Converts a `NaiveDateTime` into a `Date`.
|
|
|
|
Because `Date` does not hold time information,
|
|
data will be lost during the conversion.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.to_date(~N[2002-01-13 23:00:07])
|
|
~D[2002-01-13]
|
|
|
|
"""
|
|
def to_date(%NaiveDateTime{year: year, month: month, day: day, calendar: calendar}) do
|
|
%Date{year: year, month: month, day: day, calendar: calendar}
|
|
end
|
|
|
|
@doc """
|
|
Converts a `NaiveDateTime` into `Time`.
|
|
|
|
Because `Time` does not hold date information,
|
|
data will be lost during the conversion.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.to_time(~N[2002-01-13 23:00:07])
|
|
~T[23:00:07]
|
|
|
|
"""
|
|
def to_time(%NaiveDateTime{hour: hour, minute: minute, second: second, microsecond: microsecond}) do
|
|
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond}
|
|
end
|
|
|
|
@doc """
|
|
Converts the given naive date time to a string according to its calendar.
|
|
|
|
### Examples
|
|
|
|
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
|
|
"2000-02-28 23:00:13"
|
|
|
|
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13.001])
|
|
"2000-02-28 23:00:13.001"
|
|
|
|
"""
|
|
@spec to_string(NaiveDateTime.t) :: String.t
|
|
def to_string(%NaiveDateTime{calendar: calendar} = naive) do
|
|
calendar.to_string(naive)
|
|
end
|
|
|
|
@doc """
|
|
Parses the extended "Date and time of day" format described by ISO8601:2004.
|
|
|
|
Timezone offset may be included in the string but they will be
|
|
simply discarded as such information is not included in naive date
|
|
times.
|
|
|
|
As specified in the standard, the separator "T" may be omitted if
|
|
desired as there is no ambiguity within this function.
|
|
|
|
Time representations with reduced accuracy are not supported.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07")
|
|
{:ok, ~N[2015-01-23 23:50:07]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07")
|
|
{:ok, ~N[2015-01-23 23:50:07]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07Z")
|
|
{:ok, ~N[2015-01-23 23:50:07]}
|
|
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07.0")
|
|
{:ok, ~N[2015-01-23 23:50:07.0]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07.0123456")
|
|
{:ok, ~N[2015-01-23 23:50:07.012345]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123Z")
|
|
{:ok, ~N[2015-01-23 23:50:07.123]}
|
|
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23P23:50:07")
|
|
{:error, :invalid_format}
|
|
iex> NaiveDateTime.from_iso8601("2015:01:23 23-50-07")
|
|
{:error, :invalid_format}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07A")
|
|
{:error, :invalid_format}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:61")
|
|
{:error, :invalid_time}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-32 23:50:07")
|
|
{:error, :invalid_date}
|
|
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
|
|
{:ok, ~N[2015-01-23 23:50:07.123]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123+00:00")
|
|
{:ok, ~N[2015-01-23 23:50:07.123]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-02:30")
|
|
{:ok, ~N[2015-01-23 23:50:07.123]}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-00:00")
|
|
{:error, :invalid_format}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-00:60")
|
|
{:error, :invalid_format}
|
|
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-24:00")
|
|
{:error, :invalid_format}
|
|
|
|
"""
|
|
@spec from_iso8601(String.t) :: {:ok, NaiveDateTime.t} | {:error, atom}
|
|
def from_iso8601(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep,
|
|
hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>) when sep in [?\s, ?T] do
|
|
with {year, ""} <- Integer.parse(year),
|
|
{month, ""} <- Integer.parse(month),
|
|
{day, ""} <- Integer.parse(day),
|
|
{hour, ""} <- Integer.parse(hour),
|
|
{min, ""} <- Integer.parse(min),
|
|
{sec, ""} <- Integer.parse(sec),
|
|
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
|
|
{_offset, ""} <- Calendar.ISO.parse_offset(rest) do
|
|
new(year, month, day, hour, min, sec, microsec)
|
|
else
|
|
_ -> {:error, :invalid_format}
|
|
end
|
|
end
|
|
|
|
def from_iso8601(<<_::binary>>) do
|
|
{:error, :invalid_format}
|
|
end
|
|
|
|
@doc """
|
|
Parses the extended "Date and time of day" format described by ISO8601:2004.
|
|
|
|
Raises if the format is invalid.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.from_iso8601!("2015-01-23T23:50:07.123Z")
|
|
~N[2015-01-23 23:50:07.123]
|
|
iex> NaiveDateTime.from_iso8601!("2015-01-23P23:50:07")
|
|
** (ArgumentError) cannot parse "2015-01-23P23:50:07" as naive date time, reason: :invalid_format
|
|
|
|
"""
|
|
@spec from_iso8601!(String.t) :: NaiveDateTime.t | no_return
|
|
def from_iso8601!(string) do
|
|
case from_iso8601(string) do
|
|
{:ok, value} ->
|
|
value
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot parse #{inspect string} as naive date time, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given naive date time to ISO8601.
|
|
|
|
Only supports converting naive date times which are in the ISO calendar,
|
|
attempting to convert naive date times from other calendars will raise.
|
|
|
|
### Examples
|
|
|
|
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
|
|
"2000-02-28T23:00:13"
|
|
|
|
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13.001])
|
|
"2000-02-28T23:00:13.001"
|
|
|
|
"""
|
|
@spec to_iso8601(NaiveDateTime.t) :: String.t
|
|
def to_iso8601(%NaiveDateTime{calendar: Calendar.ISO} = naive) do
|
|
Calendar.ISO.to_iso8601(naive)
|
|
end
|
|
|
|
@doc """
|
|
Converts a `NaiveDateTime` struct to an Erlang datetime tuple.
|
|
|
|
Only supports converting naive date times which are in the ISO calendar,
|
|
attempting to convert naive date times from other calendars will raise.
|
|
|
|
WARNING: Loss of precision may occur, as Erlang time tuples only store
|
|
hour/minute/second.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.to_erl(~N[2000-01-01 13:30:15])
|
|
{{2000, 1, 1}, {13, 30, 15}}
|
|
"""
|
|
@spec to_erl(NaiveDateTime.t) :: :calendar.datetime()
|
|
def to_erl(%NaiveDateTime{calendar: Calendar.ISO, year: year, month: month, day: day,
|
|
hour: hour, minute: minute, second: second}) do
|
|
{{year, month, day}, {hour, minute, second}}
|
|
end
|
|
|
|
@doc """
|
|
Converts an Erlang datetime tuple to a `NaiveDateTime` struct.
|
|
|
|
Attempting to convert an invalid ISO calendar date will produce an error tuple.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
|
|
{:ok, ~N[2000-01-01 13:30:15]}
|
|
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
|
{:ok, ~N[2000-01-01 13:30:15.005]}
|
|
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
|
|
{:error, :invalid_date}
|
|
iex> NaiveDateTime.from_erl({{2000, 13, 1},{13, 30, 15}})
|
|
{:error, :invalid_date}
|
|
"""
|
|
@spec from_erl(:calendar.datetime(), Calendar.microsecond) :: {:ok, NaiveDateTime.t} | {:error, atom}
|
|
def from_erl({{year, month, day}, {hour, minute, second}}, microsecond \\ {0, 0}) do
|
|
new(year, month, day, hour, minute, second, microsecond)
|
|
end
|
|
|
|
@doc """
|
|
Converts an Erlang datetime tuple to a `NaiveDateTime` struct.
|
|
|
|
Raises if the datetime is invalid.
|
|
Attempting to convert an invalid ISO calendar date will produce an error tuple.
|
|
|
|
## Examples
|
|
|
|
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
|
|
~N[2000-01-01 13:30:15]
|
|
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
|
~N[2000-01-01 13:30:15.005]
|
|
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
|
|
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive date time, reason: :invalid_date
|
|
"""
|
|
@spec from_erl!(:calendar.datetime(), Calendar.microsecond) :: NaiveDateTime.t | no_return
|
|
def from_erl!(tuple, microsecond \\ {0, 0}) do
|
|
case from_erl(tuple, microsecond) do
|
|
{:ok, value} ->
|
|
value
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot convert #{inspect tuple} to naive date time, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
defimpl String.Chars do
|
|
def to_string(%NaiveDateTime{calendar: calendar} = naive) do
|
|
calendar.to_string(naive)
|
|
end
|
|
end
|
|
|
|
defimpl Inspect do
|
|
def inspect(%NaiveDateTime{calendar: Calendar.ISO} = naive, _) do
|
|
"~N[" <> Calendar.ISO.to_string(naive) <> "]"
|
|
end
|
|
|
|
def inspect(naive, opts) do
|
|
Inspect.Any.inspect(naive, opts)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule DateTime do
|
|
@moduledoc """
|
|
A datetime implementation with a time zone.
|
|
|
|
This datetime can be seen as an ephemeral snapshot
|
|
of a datetime at a given timezone. For such purposes,
|
|
it also includes both UTC and Standard offsets, as
|
|
well as the zone abbreviation field used exclusively
|
|
for formatting purposes.
|
|
|
|
Developers should avoid creating the DateTime struct directly
|
|
and instead rely on the functions provided by this module as
|
|
well as the ones in 3rd party calendar libraries.
|
|
|
|
## Where are my functions?
|
|
|
|
You will notice this module only contains conversion
|
|
functions as well as functions that work on UTC. This
|
|
is because a proper DateTime implementation requires a
|
|
TimeZone database which currently is not provided as part
|
|
of Elixir.
|
|
|
|
Such may be addressed in upcoming versions, meanwhile,
|
|
use 3rd party packages to provide DateTime building and
|
|
similar functionality with time zone backing.
|
|
"""
|
|
|
|
@enforce_keys [:year, :month, :day, :hour, :minute, :second,
|
|
:time_zone, :zone_abbr, :utc_offset, :std_offset]
|
|
defstruct [:year, :month, :day, :hour, :minute, :second, :time_zone,
|
|
:zone_abbr, :utc_offset, :std_offset, microsecond: {0, 0}, calendar: Calendar.ISO]
|
|
|
|
@type t :: %__MODULE__{year: Calendar.year, month: Calendar.month, day: Calendar.day,
|
|
calendar: Calendar.calendar, hour: Calendar.hour, minute: Calendar.minute,
|
|
second: Calendar.second, microsecond: Calendar.microsecond,
|
|
time_zone: Calendar.time_zone, zone_abbr: Calendar.zone_abbr,
|
|
utc_offset: Calendar.utc_offset, std_offset: Calendar.std_offset}
|
|
|
|
@unix_epoch :calendar.datetime_to_gregorian_seconds {{1970, 1, 1}, {0, 0, 0}}
|
|
|
|
@doc """
|
|
Returns the current datetime in UTC.
|
|
|
|
## Examples
|
|
|
|
iex> datetime = DateTime.utc_now()
|
|
iex> datetime.time_zone
|
|
"Etc/UTC"
|
|
|
|
"""
|
|
@spec utc_now() :: DateTime.t
|
|
def utc_now() do
|
|
:os.system_time |> from_unix!(:native)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given Unix time to DateTime.
|
|
|
|
The integer can be given in different unit
|
|
according to `System.convert_time_unit/3` and it will
|
|
be converted to microseconds internally.
|
|
|
|
Unix times are always in UTC and therefore the DateTime
|
|
will be returned in UTC.
|
|
|
|
## Examples
|
|
|
|
iex> DateTime.from_unix(1464096368)
|
|
{:ok, %DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {0, 0}, minute: 26,
|
|
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 2016, zone_abbr: "UTC"}}
|
|
|
|
iex> DateTime.from_unix(1432560368868569, :microseconds)
|
|
{:ok, %DateTime{calendar: Calendar.ISO, day: 25, hour: 13, microsecond: {868569, 6}, minute: 26,
|
|
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 2015, zone_abbr: "UTC"}}
|
|
|
|
The unit can also be an integer as in `System.time_unit`:
|
|
|
|
iex> DateTime.from_unix(1432560368868569, 1024)
|
|
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 22, microsecond: {211914, 3}, minute: 53,
|
|
month: 1, second: 43, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 46302, zone_abbr: "UTC"}}
|
|
|
|
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
|
|
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
|
|
|
|
iex> DateTime.from_unix(-12345678910)
|
|
{:ok, %DateTime{calendar: Calendar.ISO, day: 13, hour: 4, microsecond: {0, 0}, minute: 44,
|
|
month: 10, second: 50, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 1578, zone_abbr: "UTC"}}
|
|
|
|
When a Unix time before that moment is passed to `from_unix/2`, `:error` will be returned.
|
|
"""
|
|
@spec from_unix(integer, :native | System.time_unit) :: {:ok, DateTime.t} | {:error, atom}
|
|
def from_unix(integer, unit \\ :seconds) when is_integer(integer) do
|
|
case Calendar.ISO.from_unix(integer, unit) do
|
|
{:ok, {year, month, day}, {hour, minute, second}, microsecond} ->
|
|
{:ok, %DateTime{year: year, month: month, day: day,
|
|
hour: hour, minute: minute, second: second, microsecond: microsecond,
|
|
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
|
|
{:error, _} = error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given Unix time to DateTime.
|
|
|
|
The integer can be given in different unit
|
|
according to `System.convert_time_unit/3` and it will
|
|
be converted to microseconds internally.
|
|
|
|
Unix times are always in UTC and therefore the DateTime
|
|
will be returned in UTC.
|
|
|
|
## Examples
|
|
|
|
iex> DateTime.from_unix!(1464096368)
|
|
%DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {0, 0}, minute: 26,
|
|
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 2016, zone_abbr: "UTC"}
|
|
|
|
iex> DateTime.from_unix!(1432560368868569, :microseconds)
|
|
%DateTime{calendar: Calendar.ISO, day: 25, hour: 13, microsecond: {868569, 6}, minute: 26,
|
|
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 2015, zone_abbr: "UTC"}
|
|
|
|
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
|
|
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
|
|
|
|
iex> DateTime.from_unix(-12345678910)
|
|
{:ok, %DateTime{calendar: Calendar.ISO, day: 13, hour: 4, microsecond: {0, 0}, minute: 44,
|
|
month: 10, second: 50, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 1578, zone_abbr: "UTC"}}
|
|
|
|
When a Unix time before that moment is passed to `from_unix!/2`, an ArgumentError will be raised.
|
|
"""
|
|
@spec from_unix!(non_neg_integer, :native | System.time_unit) :: DateTime.t
|
|
def from_unix!(integer, unit \\ :seconds) when is_atom(unit) do
|
|
case from_unix(integer, unit) do
|
|
{:ok, datetime} ->
|
|
datetime
|
|
{:error, :invalid_unix_time} ->
|
|
raise ArgumentError, "invalid Unix time #{integer}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given NaiveDateTime to DateTime.
|
|
|
|
It expects a timezone to put the NaiveDateTime in.
|
|
Currently it only supports "Etc/UTC" as timezone.
|
|
|
|
## Examples
|
|
|
|
iex> DateTime.from_naive(~N[2016-05-24 13:26:08.003], "Etc/UTC")
|
|
{:ok, %DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {3000, 3}, minute: 26,
|
|
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 2016, zone_abbr: "UTC"}}
|
|
|
|
"""
|
|
@spec from_naive(NaiveDateTime.t, Calendar.time_zone) :: {:ok, DateTime.t}
|
|
def from_naive(naive_datetime, time_zone)
|
|
|
|
def from_naive(%NaiveDateTime{hour: hour, minute: minute, second: second, microsecond: microsecond,
|
|
year: year, month: month, day: day}, "Etc/UTC") do
|
|
{:ok, %DateTime{year: year, month: month, day: day,
|
|
hour: hour, minute: minute, second: second, microsecond: microsecond,
|
|
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
|
|
end
|
|
|
|
@doc """
|
|
Converts the given NaiveDateTime to DateTime.
|
|
|
|
It expects a timezone to put the NaiveDateTime in.
|
|
Currently it only supports "Etc/UTC" as timezone.
|
|
|
|
## Examples
|
|
|
|
iex> DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC")
|
|
%DateTime{calendar: Calendar.ISO, day: 24, hour: 13, microsecond: {3000, 3}, minute: 26,
|
|
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
|
year: 2016, zone_abbr: "UTC"}
|
|
|
|
"""
|
|
@spec from_naive!(non_neg_integer, :native | System.time_unit) :: DateTime.t
|
|
def from_naive!(naive_datetime, time_zone) do
|
|
case from_naive(naive_datetime, time_zone) do
|
|
{:ok, datetime} ->
|
|
datetime
|
|
{:error, reason} ->
|
|
raise ArgumentError, "cannot parse #{inspect naive_datetime} to datetime, reason: #{inspect reason}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts the given DateTime to Unix time.
|
|
|
|
The DateTime is expected to be using the ISO calendar
|
|
with a year greater than or equal to 0.
|
|
|
|
It will return the integer with the given unit,
|
|
according to `System.convert_time_unit/3`.
|
|
|
|
## Examples
|
|
|
|
iex> 1464096368 |> DateTime.from_unix!() |> DateTime.to_unix()
|
|
1464096368
|
|
|
|
iex> dt = %DateTime{calendar: Calendar.ISO, day: 20, hour: 18, microsecond: {273806, 6},
|
|
...> minute: 58, month: 11, second: 19, time_zone: "America/Montevideo",
|
|
...> utc_offset: -10800, std_offset: 3600, year: 2014, zone_abbr: "UYST"}
|
|
iex> DateTime.to_unix(dt)
|
|
1416517099
|
|
|
|
iex> flamel = %DateTime{calendar: Calendar.ISO, day: 22, hour: 8, microsecond: {527771, 6},
|
|
...> minute: 2, month: 3, second: 25, std_offset: 0, time_zone: "Etc/UTC",
|
|
...> utc_offset: 0, year: 1418, zone_abbr: "UTC"}
|
|
iex> DateTime.to_unix(flamel)
|
|
-17412508655
|
|
|
|
"""
|
|
@spec to_unix(DateTime.t, System.time_unit) :: non_neg_integer
|
|
def to_unix(datetime, unit \\ :seconds)
|
|
|
|
def to_unix(%DateTime{calendar: Calendar.ISO, std_offset: std_offset, utc_offset: utc_offset,
|
|
hour: hour, minute: minute, second: second, microsecond: {microsecond, _},
|
|
year: year, month: month, day: day}, unit) when year >= 0 do
|
|
seconds =
|
|
:calendar.datetime_to_gregorian_seconds({{year, month, day}, {hour, minute, second}})
|
|
|> Kernel.-(utc_offset)
|
|
|> Kernel.-(std_offset)
|
|
System.convert_time_unit((seconds - @unix_epoch) * 1_000_000 + microsecond, :microseconds, unit)
|
|
end
|
|
|
|
@doc """
|
|
Converts a `DateTime` into a `NaiveDateTime`.
|
|
|
|
Because `NaiveDateTime` does not hold timezone information,
|
|
any timezone related data will be lost during the conversion.
|
|
|
|
## Examples
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 1},
|
|
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
|
iex> DateTime.to_naive(dt)
|
|
~N[2000-02-29 23:00:07.0]
|
|
|
|
"""
|
|
def to_naive(%DateTime{year: year, month: month, day: day, calendar: calendar,
|
|
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
|
|
%NaiveDateTime{year: year, month: month, day: day, calendar: calendar,
|
|
hour: hour, minute: minute, second: second, microsecond: microsecond}
|
|
end
|
|
|
|
@doc """
|
|
Converts a `DateTime` into a `Date`.
|
|
|
|
Because `Date` does not hold time nor timezone information,
|
|
data will be lost during the conversion.
|
|
|
|
## Examples
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
|
iex> DateTime.to_date(dt)
|
|
~D[2000-02-29]
|
|
|
|
"""
|
|
def to_date(%DateTime{year: year, month: month, day: day, calendar: calendar}) do
|
|
%Date{year: year, month: month, day: day, calendar: calendar}
|
|
end
|
|
|
|
@doc """
|
|
Converts a `DateTime` into `Time`.
|
|
|
|
Because `Time` does not hold date nor timezone information,
|
|
data will be lost during the conversion.
|
|
|
|
## Examples
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 1},
|
|
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
|
iex> DateTime.to_time(dt)
|
|
~T[23:00:07.0]
|
|
|
|
"""
|
|
def to_time(%DateTime{hour: hour, minute: minute, second: second, microsecond: microsecond}) do
|
|
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond}
|
|
end
|
|
|
|
@doc """
|
|
Converts the given date time to ISO8601.
|
|
|
|
Only supports converting date times which are in the ISO calendar,
|
|
attempting to convert date times from other calendars will raise.
|
|
|
|
WARNING: the ISO8601 does not contain the time zone nor its abbreviation,
|
|
which means information is lost when converting to such format. This
|
|
is also why this module does not provide a `from_iso8601/1` function,
|
|
as it is impossible to build a proper `DateTime` from only the
|
|
information in the ISO8601 string.
|
|
|
|
### Examples
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
|
iex> DateTime.to_iso8601(dt)
|
|
"2000-02-29T23:00:07+01:00"
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "UTC",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
|
iex> DateTime.to_iso8601(dt)
|
|
"2000-02-29T23:00:07Z"
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
|
|
iex> DateTime.to_iso8601(dt)
|
|
"2000-02-29T23:00:07-04:00"
|
|
"""
|
|
@spec to_iso8601(DateTime.t) :: String.t
|
|
def to_iso8601(%DateTime{calendar: Calendar.ISO} = dt) do
|
|
Calendar.ISO.to_iso8601(dt)
|
|
end
|
|
|
|
@doc """
|
|
Converts the given date time to a string according to its calendar.
|
|
|
|
### Examples
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
|
iex> DateTime.to_string(dt)
|
|
"2000-02-29 23:00:07+01:00 CET Europe/Warsaw"
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "UTC",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
|
iex> DateTime.to_string(dt)
|
|
"2000-02-29 23:00:07Z"
|
|
|
|
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
|
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
|
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
|
|
iex> DateTime.to_string(dt)
|
|
"2000-02-29 23:00:07-04:00 AMT America/Manaus"
|
|
"""
|
|
@spec to_string(DateTime.t) :: String.t
|
|
def to_string(%DateTime{calendar: calendar} = dt) do
|
|
calendar.to_string(dt)
|
|
end
|
|
|
|
defimpl String.Chars do
|
|
def to_string(%DateTime{calendar: calendar} = dt) do
|
|
calendar.to_string(dt)
|
|
end
|
|
end
|
|
end
|