summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@plataformatec.com.br>2016-09-14 23:28:34 +0200
committerJosé Valim <jose.valim@plataformatec.com.br>2016-09-14 23:29:18 +0200
commite50735ff2a7c16cc7705d8fa59af906e986c19f7 (patch)
treed2b2de83282e1252f1bb335abb5258b0cff781b4
parent72e9f9da01afe9c8ad57d3ceca26d3c5cd54289b (diff)
downloadelixir-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.ex228
-rw-r--r--lib/elixir/lib/calendar/iso.ex54
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