Current section

Files

Jump to
timex lib date date.ex
Raw

lib/date/date.ex

defmodule Timex.Date do
@moduledoc """
This module represents all functions specific to creating/manipulating/comparing Dates (year/month/day)
"""
defstruct calendar: :gregorian, day: 1, month: 1, year: 0
alias __MODULE__
alias Timex.DateTime
alias Timex.TimezoneInfo
alias Timex.Helpers
use Timex.Constants
import Timex.Macros
@type t :: %__MODULE__{}
@doc """
Returns today's date as a Date struct. If given a timezone, returns whatever "today" is in that timezone
"""
@spec today() :: Date.t | {:error, term}
@spec today(Types.valid_timezone) :: Date.t | {:error, term}
def today, do: now
def today(%TimezoneInfo{} = tz), do: now(tz)
def today(tz) when is_binary(tz), do: now(tz)
def today(tz) when tz in [:utc, :local], do: now(tz)
def today(_), do: {:error, :invalid_timezone}
@doc """
Returns today's date as a Date struct. If given a timezone, returns whatever "today" is in that timezone
"""
@spec now() :: Date.t | {:error, term}
@spec now(Types.valid_timezone) :: DateTime.t | {:error, term}
def now, do: from_erl(Helpers.calendar_universal_time())
def now(tz) when is_binary(tz), do: from(DateTime.now(tz))
def now(%TimezoneInfo{} = tz), do: from(DateTime.now(tz))
def now(tz) when tz in [:utc, :local], do: from(DateTime.now(tz))
def now(:days), do: to_days(now())
def now(_), do: {:error, :invalid_timezone}
@doc """
Returns a Date representing the first day of year zero
"""
@spec zero() :: Date.t
def zero, do: from_erl({0, 1, 1})
@doc """
Returns a Date representing the date of the UNIX epoch
"""
@spec epoch() :: DateTime.t
@spec epoch(:seconds) :: DateTime.t
def epoch, do: from_erl({1970, 1, 1})
def epoch(:seconds), do: to_seconds(from_erl({1970, 1, 1}), :zero)
def epoch(:secs) do
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead\n"
epoch(:seconds)
end
@doc """
Converts from a date/time value to a Date struct representing that date
"""
@spec from(Types.valid_datetime | Types.dtz | Types.phoenix_datetime_select_params) :: Date.t | {:error, term}
# From Timex types
def from(%Date{} = date), do: date
def from(%DateTime{year: y, month: m, day: d}), do: %Date{year: y, month: m, day: d}
# From Erlang/Ecto datetime tuples
def from({y,m,d} = date) when is_date(y,m,d),
do: from_erl(date)
def from({{y,m,d} = date, {h,mm,s}}) when is_datetime(y,m,d,h,mm,s),
do: from_erl(date)
def from({{y,m,d} = date, {h,mm,s,ms}}) when is_datetime(y,m,d,h,mm,s,ms),
do: from_erl(date)
# Phoenix datetime select value
def from(%{"year" => _, "month" => _, "day" => _} = dt) 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 do
%{"year" => y, "month" => m, "day" => d} ->
from({{y,m,d},{0,0,0}})
{:error, _} ->
{:error, :invalid}
end
end
def from(_), do: {:error, :invalid_date}
@doc """
WARNING: This is here to ease the migration to 2.x, but is deprecated.
Converts a value of the provided type to a Date struct, relative to the reference date (:epoch or :zero)
"""
def from(value, type, ref \\ :epoch)
defdeprecated from(ts, :timestamp, ref), "use Date.from_timestamp/1 instead",
do: from_timestamp(ts, ref)
defdeprecated from(n, :us, ref), "use Date.from_microseconds/1 instead",
do: from_microseconds(n, ref)
defdeprecated from(n, :msecs, ref), "use Date.from_milliseconds/1 instead",
do: from_milliseconds(n, ref)
defdeprecated from(n, :secs, ref), "use Date.from_seconds/1 instead",
do: from_seconds(n, ref)
defdeprecated from(n, :days, ref), "use Date.from_days/1 instead",
do: from_days(n, ref)
@doc """
Like from/1, but more explicit about it's inputs (Erlang date/datetime tuples only).
"""
def from_erl({y,m,d}) when is_date(y,m,d) do
case :calendar.valid_date({y,m,d}) do
true -> %Date{year: y, month: m, day: d}
false -> {:error, :invalid_date}
end
end
def from_erl({{y,m,d}, {_,_,_}}) when is_date(y,m,d), do: from_erl({y,m,d})
def from_erl({{y,m,d}, {_,_,_,_}}) when is_date(y,m,d), do: from_erl({y,m,d})
def from_erl(_), do: {:error, :invalid_date}
@doc """
Given an Erlang timestamp, converts it to a Date struct representing the date of that timestamp
"""
@spec from_timestamp(Types.timestamp, :epoch | :zero) :: Date.t | {:error, term}
def from_timestamp(timestamp, ref \\ :epoch)
def from_timestamp({mega,sec,micro} = timestamp, ref)
when is_date_timestamp(mega,sec,micro) and ref in [:epoch, :zero]
do
case ok!(DateTime.from_timestamp(timestamp, ref)) do
{:error, _} = err -> err
{:ok, datetime} -> from(datetime)
end
end
def from_timestamp(_, _), do: {:error, :badarg}
@doc """
Given an integer value representing days since the reference date (:epoch or :zero), returns
a Date struct representing that date
"""
@spec from_days(non_neg_integer, :epoch | :zero) :: Date.t | {:error, term}
def from_days(n, ref \\ :epoch)
def from_days(n, ref) when is_positive_number(n) and ref in [:epoch, :zero] do
case ok!(DateTime.from_days(trunc(n), ref)) do
{:error, _} = err -> err
{:ok, datetime} -> from(datetime)
end
end
def from_days(_, _), do: {:error, :badarg}
@doc """
Given an integer value representing seconds since the reference date (:epoch or :zero), returns
a Date struct representing that date
"""
@spec from_seconds(non_neg_integer, :epoch | :zero) :: Date.t | {:error, term}
def from_seconds(n, ref \\ :epoch)
def from_seconds(n, ref) when is_positive_number(n) and ref in [:epoch, :zero] do
case ok!(DateTime.from_seconds(trunc(n), ref)) do
{:error, _} = err -> err
{:ok, datetime} -> from(datetime)
end
end
def from_seconds(_, _), do: {:error, :badarg}
@doc """
Given an integer value representing milliseconds since the reference date (:epoch or :zero), returns
a Date struct representing that date
"""
@spec from_milliseconds(non_neg_integer, :epoch | :zero) :: Date.t | {:error, term}
def from_milliseconds(n, ref \\ :epoch)
def from_milliseconds(n, ref) when is_positive_number(n) and ref in [:epoch, :zero] do
case ok!(DateTime.from_milliseconds(trunc(n), ref)) do
{:error, _} = err -> err
{:ok, datetime} -> from(datetime)
end
end
def from_millisecond(_, _), do: {:error, :badarg}
@doc """
Given an integer value representing microseconds since the reference date (:epoch or :zero), returns
a Date struct representing that date
"""
@spec from_microseconds(non_neg_integer, :epoch | :zero) :: Date.t | {:error, term}
def from_microseconds(n, ref \\ :epoch)
def from_microseconds(n, ref) when is_positive_number(n) and ref in [:epoch, :zero] do
case ok!(DateTime.from_microseconds(trunc(n), ref)) do
{:error, _} = err -> err
{:ok, datetime} -> from(datetime)
end
end
def from_microseconds(_, _), do: {:error, :badarg}
@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(Date.t) :: Types.timestamp | {:error, term}
@spec to_timestamp(Date.t, :epoch | :zero) :: Types.timestamp | {:error, term}
def to_timestamp(date, ref \\ :epoch)
def to_timestamp(%Date{} = date, ref) when ref in [:epoch, :zero] do
case ok!(to_datetime(date)) do
{:error, _} = err -> err
{:ok, datetime} -> DateTime.to_timestamp(datetime, ref)
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
@doc """
Convert a date to an integer number of seconds since Epoch or year 0.
See also `Timex.diff/3` if you want to specify an arbitrary reference date.
## Examples
iex> Timex.date({1999, 1, 2}) |> #{__MODULE__}.to_seconds
915235200
"""
@spec to_seconds(Date.t) :: integer | {:error, term}
@spec to_seconds(Date.t, :epoch | :zero) :: integer | {:error, term}
def to_seconds(date, ref \\ :epoch)
def to_seconds(%Date{} = date, ref) when ref in [:epoch, :zero] do
case ok!(to_datetime(date)) do
{:error, _} = err -> err
{:ok, datetime} -> DateTime.to_seconds(datetime, ref)
end
end
def to_seconds(_, _), do: {:error, :badarg}
@doc """
Convert the date to an integer number of days since Epoch or year 0.
See also `Timex.diff/3` if you want to specify an arbitray reference date.
## Examples
iex> Timex.date({1970, 1, 15}) |> #{__MODULE__}.to_days
14
"""
@spec to_days(Date.t) :: integer | {:error, term}
@spec to_days(Date.t, :epoch | :zero) :: integer | {:error, term}
def to_days(date, ref \\ :epoch)
def to_days(date, ref) when ref in [:epoch, :zero] do
case ok!(to_datetime(date)) do
{:error, _} = err -> err
{:ok, datetime} -> DateTime.to_days(datetime, ref)
end
end
def to_days(_, _), do: {:error, :badarg}
@doc """
Converts a Date to a DateTime in UTC
"""
@spec to_datetime(Date.t) :: DateTime.t | {:error, term}
def to_datetime(%DateTime{} = dt), do: dt
def to_datetime(%Date{:year => y, :month => m, :day => d}) do
%DateTime{:year => y, :month => m, :day => d, :timezone => %TimezoneInfo{}}
end
def to_datetime(_), do: {:error, :badarg}
@doc """
See docs for Timex.set/2 for details.
"""
@spec set(Date.t, list({atom(), term})) :: Date.t | {:error, term}
def set(%Date{} = 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, %Date{} = result ->
case option do
{:validate, _} -> result
{:datetime, {{y, m, d}, {_, _, _}}} ->
if validate? do
%{result |
:year => Timex.normalize(:year, y),
:month => Timex.normalize(:month, m),
:day => Timex.normalize(:day, {y,m,d}),
}
else
%{result | :year => y, :month => m, :day => d}
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
{:day, d} ->
if validate? do
%{result | :day => Timex.normalize(:day, {result.year, result.month, d})}
else
%{result | :day => d}
end
{name, val} when name in [:year, :month] ->
if validate? do
Map.put(result, name, Timex.normalize(name, val))
else
Map.put(result, name, val)
end
{:ms, _} ->
IO.write :stderr, "warning: using :ms with shift is deprecated, use :millisecond instead"
result
{name, _} when name in [:time, :timezone, :hour, :minute, :second] ->
result
{option_name, _} ->
{:error, {:invalid_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(Date.t, Date.t | :epoch | :zero | :distant_past | :distant_future) :: -1 | 0 | 1 | {:error, term}
@spec compare(Date.t, Date.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(a, b), do: DateTime.compare(to_datetime(a), to_datetime(b))
def compare(a, b, granularity), do: DateTime.compare(to_datetime(a), to_datetime(b), 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(Date.t, Date.t, :timestamp) :: Types.timestamp | {:error, term}
@spec diff(Date.t, Date.t, :seconds | :minutes | :hours | :days | :weeks | :months | :years) :: integer | {:error, term}
def diff(this, other, type) do
DateTime.diff(to_datetime(this), to_datetime(other), type)
end
@doc """
Shifts the given Date based on the provided options.
See Timex.shift/2 for more information.
"""
@spec shift(Date.t, list({atom(), term})) :: Date.t | {:error, term}
def shift(%Date{} = date, [{_, 0}]), do: date
def shift(%Date{} = date, [timestamp: {0,0,0}]), do: date
def shift(%Date{} = date, options) do
allowed_options = Enum.filter(options, fn
{:hours, value} when value >= 24 or value <= -24 -> true
{:hours, _} -> false
{:mins, value} when value >= 24*60 or value <= -24*60 ->
IO.write :stderr, "warning: :mins is a deprecated unit name, use :minutes instead"
true
{:mins, _} ->
IO.write :stderr, "warning: :mins is a deprecated unit name, use :minutes instead"
false
{:minutes, value} when value >= 24*60 or value <= -24*60 -> true
{:minutes, _} -> false
{:secs, value} when value >= 24*60*60 or value <= -24*60*60 ->
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead"
true
{:secs, _} ->
IO.write :stderr, "warning: :secs is a deprecated unit name, use :seconds instead"
false
{:seconds, value} when value >= 24*60*60 or value <= -24*60*60 -> true
{:seconds, _} -> false
{:msecs, value} when value >= 24*60*60*1000 or value <= -24*60*60*1000 ->
IO.write :stderr, "warning: :msecs is a deprecated unit name, use :milliseconds instead"
true
{:msecs, _} ->
IO.write :stderr, "warning: :msecs is a deprecated unit name, use :milliseconds instead"
false
{:milliseconds, value} when value >= 24*60*60*1000 or value <= -24*60*60*1000 -> true
{:milliseconds, _} -> false
{_type, _value} -> true
end)
case DateTime.shift(to_datetime(date), allowed_options) do
{:error, _} = err -> err
datetime -> from(datetime)
end
end
def shift(_, _), do: {:error, :badarg}
end