From e50735ff2a7c16cc7705d8fa59af906e986c19f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 14 Sep 2016 23:28:34 +0200 Subject: [PATCH] Augment the Calendar APIs with utc date/time and last_day_of_the_month Closes #5216 --- lib/elixir/lib/calendar.ex | 228 ++++++++++++++++++++++++++------- lib/elixir/lib/calendar/iso.ex | 54 ++++++++ 2 files changed, 237 insertions(+), 45 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index f0217cade2..87a34e4c8e 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -55,6 +55,11 @@ defmodule Calendar do """ @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. @@ -68,26 +73,6 @@ defmodule Calendar do Converts the given structure into a string according to the calendar. """ @callback to_string(structure :: Date.t | DateTime.t | NaiveDateTime.t) :: String.t - - @doc false - # TODO: Remove this on 1.4. It exists only to aid migration of those - # using the Calendar library. - defmacro __using__(_opts) do - %{file: file, line: line} = __CALLER__ - :elixir_errors.warn(line, file, "use Calendar is deprecated as it is now part of Elixir") - - quote do - alias Calendar.DateTime - alias Calendar.DateTime.Interval - alias Calendar.AmbiguousDateTime - alias Calendar.NaiveDateTime - alias Calendar.Date - alias Calendar.Time - alias Calendar.TimeZoneData - alias Calendar.TzPeriod - alias Calendar.Strftime - end - end end defmodule Date do @@ -121,6 +106,22 @@ defmodule Date do @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 = Time.utc_now() + 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. @@ -337,6 +338,22 @@ defmodule Time do @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. @@ -609,6 +626,144 @@ defmodule NaiveDateTime do 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. 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[2015-05-25 13:26:08]} + + iex> NaiveDateTime.from_unix(1432560368868569, :microseconds) + {: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, 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> NaiveDateTime.from_unix(-12345678910) + {:ok, ~N[1578-10-13 04:10: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 \\ :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 + 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[2015-05-25 13:26:08] + + iex> NaiveDateTime.from_unix!(1432560368868569, :microseconds) + ~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:10: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 \\ :seconds) when is_atom(unit) do + case from_unix(integer, unit) do + {:ok, datetime} -> + datetime + :error -> + raise ArgumentError, "invalid Unix time #{integer}" + end + end + + @doc """ + Converts the given NaiveDateTime to Unix seconds. + + The NaiveDateTime 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`. + + WARNING: A Unix time is always in UTC. However, NaiveDateTime + does not have time zone information, which means there is no + guarantee the Unix time retrieved is the one originally intended. + This may result in incorrect results. Prefer using `DateTime` + when converting to and from Unix time as it includes the time + zone data. + + ## Examples + + iex> 1464096368 |> NaiveDateTime.from_unix!() |> NaiveDateTime.to_unix() + 1464096368 + + iex> NaiveDateTime.to_unix(~N[2015-05-25 13:26:08]) + 1464096368 + + """ + @spec to_unix(NaiveDateTime.t, System.time_unit) :: non_neg_integer + def to_unix(datetime, unit \\ :seconds) + + def to_unix(%NaiveDateTime{calendar: Calendar.ISO, year: year, month: month, day: day, + hour: hour, minute: minute, second: second, microsecond: {microsecond, _}}, unit) when year >= 0 do + seconds = :calendar.datetime_to_gregorian_seconds({{year, month, day}, {hour, minute, second}}) + System.convert_time_unit((seconds - @unix_epoch) * 1_000_000 + microsecond, :microseconds, unit) + end + @doc """ Builds a new ISO naive date time. @@ -1017,33 +1172,16 @@ defmodule DateTime do """ @spec from_unix(integer, :native | System.time_unit) :: {:ok, DateTime.t} def from_unix(integer, unit \\ :seconds) when is_integer(integer) do - total = System.convert_time_unit(integer, unit, :microseconds) - if total < -@unix_epoch * 1_000_000 do - :error - else - microsecond = rem(total, 1_000_000) - precision = precision_for_unit(unit) - {{year, month, day}, {hour, minute, second}} = - :calendar.gregorian_seconds_to_datetime(@unix_epoch + div(total, 1_000_000)) - - {:ok, %DateTime{year: year, month: month, day: day, - hour: hour, minute: minute, second: second, microsecond: {microsecond, precision}, - std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}} + 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 end end - defp precision_for_unit(unit) do - subseconds = div System.convert_time_unit(1, :seconds, unit), 10 - precision_for_unit(subseconds, 0) - end - - defp precision_for_unit(0, precision), - do: precision - defp precision_for_unit(_, 6), - do: 6 - defp precision_for_unit(number, precision), - do: precision_for_unit(div(number, 10), precision + 1) - @doc """ Converts the given Unix time to DateTime. @@ -1087,7 +1225,7 @@ defmodule DateTime do end @doc """ - Converts the given DateTime to Unix time. + Converts the given DateTime to Unix seconds. The DateTime is expected to be using the ISO calendar with a year greater than or equal to 0. diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 44d60e6a1f..93b40f69a2 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -37,6 +37,35 @@ defmodule Calendar.ISO do end end + + @doc """ + Returns the last day of the month for the given year. + + ## Examples + + iex> Calendar.ISO.last_day_of_month(2000, 1) + 31 + iex> Calendar.ISO.last_day_of_month(2000, 2) + 28 + iex> Calendar.ISO.last_day_of_month(2001, 2) + 28 + iex> Calendar.ISO.last_day_of_month(2004, 2) + 29 + iex> Calendar.ISO.last_day_of_month(2004, 4) + 30 + + """ + def last_day_of_the_month(year, month) + + def last_day_of_the_month(year, 2) do + if leap_year?(year), do: 29, else: 28 + end + def last_day_of_the_month(_, 4), do: 30 + def last_day_of_the_month(_, 6), do: 30 + def last_day_of_the_month(_, 9), do: 30 + def last_day_of_the_month(_,11), do: 30 + def last_day_of_the_month(_, month) when month in 1..12, do: 31 + @doc """ Returns if the given year is a leap year. @@ -121,6 +150,31 @@ defmodule Calendar.ISO do ## Helpers + @doc false + def from_unix(integer, unit) when is_integer(integer) do + total = System.convert_time_unit(integer, unit, :microseconds) + if total < -@unix_epoch * 1_000_000 do + :error + else + microsecond = rem(total, 1_000_000) + precision = precision_for_unit(unit) + {date, time} = :calendar.gregorian_seconds_to_datetime(@unix_epoch + div(total, 1_000_000)) + {:ok, date, time, {microsecond, precision}} + end + end + + defp precision_for_unit(unit) do + subseconds = div System.convert_time_unit(1, :seconds, unit), 10 + precision_for_unit(subseconds, 0) + end + + defp precision_for_unit(0, precision), + do: precision + defp precision_for_unit(_, 6), + do: 6 + defp precision_for_unit(number, precision), + do: precision_for_unit(div(number, 10), precision + 1) + @doc false def to_iso8601(%Date{year: year, month: month, day: day}) do date_to_string(year, month, day)