diff options
author | José Valim <jose.valim@plataformatec.com.br> | 2016-09-14 23:28:34 +0200 |
---|---|---|
committer | José Valim <jose.valim@plataformatec.com.br> | 2016-09-14 23:29:18 +0200 |
commit | e50735ff2a7c16cc7705d8fa59af906e986c19f7 (patch) | |
tree | d2b2de83282e1252f1bb335abb5258b0cff781b4 | |
parent | 72e9f9da01afe9c8ad57d3ceca26d3c5cd54289b (diff) | |
download | elixir-e50735ff2a7c16cc7705d8fa59af906e986c19f7.tar.gz |
Augment the Calendar APIs with utc date/time and last_day_of_the_month
Closes #5216
-rw-r--r-- | lib/elixir/lib/calendar.ex | 228 | ||||
-rw-r--r-- | 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 f0217cade..87a34e4c8 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -56,6 +56,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. A leap year is a year of a longer length than normal. The exact meaning @@ -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 @@ -122,6 +107,22 @@ defmodule Date do 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. Expects all values to be integers. Returns `{:ok, date}` if each @@ -338,6 +339,22 @@ defmodule Time do 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 @@ -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 44d60e6a1..93b40f69a 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. @@ -122,6 +151,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) end |