Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ~U sigil for UTC date times #8824

Merged
merged 10 commits into from
Feb 25, 2019
51 changes: 29 additions & 22 deletions lib/elixir/lib/calendar/datetime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,17 @@ defmodule DateTime do

iex> {:ok, datetime} = DateTime.from_unix(1_464_096_368)
iex> datetime
#DateTime<2016-05-24 13:26:08Z>
~U[2016-05-24 13:26:08Z]

iex> {:ok, datetime} = DateTime.from_unix(1_432_560_368_868_569, :microsecond)
iex> datetime
#DateTime<2015-05-25 13:26:08.868569Z>
~U[2015-05-25 13:26:08.868569Z]

The unit can also be an integer as in `t:System.time_unit/0`:

iex> {:ok, datetime} = DateTime.from_unix(143_256_036_886_856, 1024)
iex> datetime
#DateTime<6403-03-17 07:05:22.320312Z>
~U[6403-03-17 07:05:22.320312Z]

Negative Unix times are supported, up to -62167219200 seconds,
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
Expand Down Expand Up @@ -152,16 +152,17 @@ defmodule DateTime do

# An easy way to get the Unix epoch is passing 0 to this function
iex> DateTime.from_unix!(0)
#DateTime<1970-01-01 00:00:00Z>
~U[1970-01-01 00:00:00Z]

iex> DateTime.from_unix!(1_464_096_368)
#DateTime<2016-05-24 13:26:08Z>
~U[2016-05-24 13:26:08Z]

iex> DateTime.from_unix!(1_432_560_368_868_569, :microsecond)
#DateTime<2015-05-25 13:26:08.868569Z>
~U[2015-05-25 13:26:08.868569Z]

iex> DateTime.from_unix!(143_256_036_886_856, 1024)
#DateTime<6403-03-17 07:05:22.320312Z>
~U[6403-03-17 07:05:22.320312Z]

"""
@spec from_unix!(integer, :native | System.time_unit(), Calendar.calendar()) :: t
def from_unix!(integer, unit \\ :second, calendar \\ Calendar.ISO) do
Expand All @@ -185,9 +186,8 @@ defmodule DateTime do

## Examples

iex> {:ok, datetime} = DateTime.from_naive(~N[2016-05-24 13:26:08.003], "Etc/UTC")
iex> datetime
#DateTime<2016-05-24 13:26:08.003Z>
iex> DateTime.from_naive(~N[2016-05-24 13:26:08.003], "Etc/UTC")
{:ok, ~U[2016-05-24 13:26:08.003Z]}

When the datetime is ambiguous - for instance during changing from summer
to winter time - the two possible valid datetimes are returned. First the one
Expand Down Expand Up @@ -229,7 +229,7 @@ defmodule DateTime do
iex> cph_datetime = DateTime.from_naive!(~N[2018-08-24 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> {:ok, utc_datetime} = DateTime.from_naive(cph_datetime, "Etc/UTC", FakeTimeZoneDatabase)
iex> utc_datetime
#DateTime<2018-08-24 10:00:00Z>
~U[2018-08-24 10:00:00Z]

If instead you want a `DateTime` for the same point time in a different time zone see the
`DateTime.shift_zone/3` function which would convert 2018-08-24 10:00:00 in Copenhagen
Expand Down Expand Up @@ -358,7 +358,7 @@ defmodule DateTime do
## Examples

iex> DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC")
#DateTime<2016-05-24 13:26:08.003Z>
~U[2016-05-24 13:26:08.003Z]

iex> DateTime.from_naive!(~N[2018-05-24 13:26:08.003], "Europe/Copenhagen", FakeTimeZoneDatabase)
#DateTime<2018-05-24 13:26:08.003+02:00 CEST Europe/Copenhagen>
Expand Down Expand Up @@ -749,23 +749,23 @@ defmodule DateTime do

iex> {:ok, datetime, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z")
iex> datetime
#DateTime<2015-01-23 23:50:07Z>
~U[2015-01-23 23:50:07Z]

iex> {:ok, datetime, 9000} = DateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
iex> datetime
#DateTime<2015-01-23 21:20:07.123Z>
~U[2015-01-23 21:20:07.123Z]

iex> {:ok, datetime, 9000} = DateTime.from_iso8601("2015-01-23T23:50:07,123+02:30")
iex> datetime
#DateTime<2015-01-23 21:20:07.123Z>
~U[2015-01-23 21:20:07.123Z]

iex> {:ok, datetime, 0} = DateTime.from_iso8601("-2015-01-23T23:50:07Z")
iex> datetime
#DateTime<-2015-01-23 23:50:07Z>
~U[-2015-01-23 23:50:07Z]

iex> {:ok, datetime, 9000} = DateTime.from_iso8601("-2015-01-23T23:50:07,123+02:30")
iex> datetime
#DateTime<-2015-01-23 21:20:07.123Z>
~U[-2015-01-23 21:20:07.123Z]

iex> DateTime.from_iso8601("2015-01-23P23:50:07")
{:error, :invalid_format}
Expand Down Expand Up @@ -1038,9 +1038,8 @@ defmodule DateTime do
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
#DateTime<2018-11-15 11:00:00+01:00 CET Europe/Copenhagen>

iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Etc/UTC")
iex> dt |> DateTime.add(3600, :second)
#DateTime<2018-11-15 11:00:00Z>
iex> DateTime.add(~U[2018-11-15 10:00:00Z], 3600, :second)
~U[2018-11-15 11:00:00Z]

When adding 3 seconds just before "spring forward" we go from 1:59:59 to 3:00:02

Expand Down Expand Up @@ -1320,7 +1319,7 @@ defmodule DateTime do
std_offset: std_offset
} = datetime

"#DateTime<" <>
formatted =
Calendar.ISO.datetime_to_string(
year,
month,
Expand All @@ -1333,7 +1332,15 @@ defmodule DateTime do
zone_abbr,
utc_offset,
std_offset
) <> ">"
)

case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
"~U[" <> formatted <> "]"

_ ->
"#DateTime<" <> formatted <> ">"
end
end

def inspect(datetime, opts) do
Expand Down
43 changes: 42 additions & 1 deletion lib/elixir/lib/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4980,7 +4980,7 @@ defmodule Kernel do
Handles the sigil `~N` for naive date times.

The lower case `~n` variant does not exist as interpolation
and escape characters are not useful for datetime sigils.
and escape characters are not useful for date time sigils.

More information on naive date times can be found in the `NaiveDateTime` module.

Expand All @@ -4998,6 +4998,47 @@ defmodule Kernel do
Macro.escape(NaiveDateTime.from_iso8601!(string))
end

@doc ~S"""
Handles the sigil `~U` to create a UTC `DateTime`.

The lower case `~u` variant does not exist as interpolation
and escape characters are not useful for date time sigils.

The given `datetime_string` must include "Z" or "00:00" offset which marks it
as UTC, otherwise an error is raised.

josevalim marked this conversation as resolved.
Show resolved Hide resolved
More information on date times can be found in the `DateTime` module.

## Examples

iex> ~U[2015-01-13 13:00:07Z]
~U[2015-01-13 13:00:07Z]
iex> ~U[2015-01-13T13:00:07.001+00:00]
~U[2015-01-13 13:00:07.001Z]

"""
@doc since: "1.9.0"
defmacro sigil_U(datetime_string, modifiers)

defmacro sigil_U({:<<>>, _, [string]}, []) do
Macro.escape(datetime_from_utc_iso8601!(string))
josevalim marked this conversation as resolved.
Show resolved Hide resolved
end

defp datetime_from_utc_iso8601!(string) do
case DateTime.from_iso8601(string) do
{:ok, utc_datetime, 0} ->
utc_datetime

{:ok, _datetime, _offset} ->
raise ArgumentError,
"cannot parse #{inspect(string)} as UTC datetime, reason: :non_utc_offset"

{:error, reason} ->
raise ArgumentError,
"cannot parse #{inspect(string)} as UTC datetime, reason: #{inspect(reason)}"
end
end

@doc ~S"""
Handles the sigil `~w` for list of words.

Expand Down
29 changes: 29 additions & 0 deletions lib/elixir/test/elixir/kernel_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,35 @@ defmodule KernelTest do
assert match?(x when ceil(x) == 1, 0.2)
end

test "sigil_U/2" do
assert ~U[2015-01-13 13:00:07.123Z] == %DateTime{
calendar: Calendar.ISO,
day: 13,
hour: 13,
microsecond: {123_000, 3},
minute: 0,
month: 1,
second: 7,
std_offset: 0,
time_zone: "Etc/UTC",
utc_offset: 0,
year: 2015,
zone_abbr: "UTC"
}

assert_raise ArgumentError, ~r"reason: :invalid_format", fn ->
Code.eval_string(~s{~U[2015-01-13 13:00]})
end

assert_raise ArgumentError, ~r"reason: :missing_offset", fn ->
Code.eval_string(~s{~U[2015-01-13 13:00:07]})
end

assert_raise ArgumentError, ~r"reason: :non_utc_offset", fn ->
Code.eval_string(~s{~U[2015-01-13 13:00:07+00:30]})
end
end

josevalim marked this conversation as resolved.
Show resolved Hide resolved
defp purge(module) do
:code.delete(module)
:code.purge(module)
Expand Down