Skip to content

Commit 13b432c

Browse files
QqwyJosé Valim
authored andcommitted
Enhancement: negative integer support in DateTime.from_unix (#5128)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
1 parent effb685 commit 13b432c

File tree

2 files changed

+71
-13
lines changed

2 files changed

+71
-13
lines changed

lib/elixir/lib/calendar.ex

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,19 +1004,32 @@ defmodule DateTime do
10041004
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 22, microsecond: {211914, 3}, minute: 53,
10051005
month: 1, second: 43, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
10061006
year: 46302, zone_abbr: "UTC"}}
1007-
1007+
1008+
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
1009+
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
1010+
1011+
iex> DateTime.from_unix(-12345678910)
1012+
{:ok, %DateTime{calendar: Calendar.ISO, day: 13, hour: 4, microsecond: {0, 0}, minute: 44,
1013+
month: 10, second: 50, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
1014+
year: 1578, zone_abbr: "UTC"}}
1015+
1016+
When a Unix time before that moment is passed to `from_unix/2`, `:error` will be returned.
10081017
"""
1009-
@spec from_unix(non_neg_integer, :native | System.time_unit) :: {:ok, DateTime.t}
1010-
def from_unix(integer, unit \\ :seconds) when is_integer(integer) and integer >= 0 do
1018+
@spec from_unix(integer, :native | System.time_unit) :: {:ok, DateTime.t}
1019+
def from_unix(integer, unit \\ :seconds) when is_integer(integer) do
10111020
total = System.convert_time_unit(integer, unit, :microseconds)
1012-
microsecond = rem(total, 1_000_000)
1013-
precision = precision_for_unit(unit)
1014-
{{year, month, day}, {hour, minute, second}} =
1015-
:calendar.gregorian_seconds_to_datetime(@unix_epoch + div(total, 1_000_000))
1016-
1017-
{:ok, %DateTime{year: year, month: month, day: day,
1018-
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision},
1019-
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
1021+
if total < -@unix_epoch * 1_000_000 do
1022+
:error
1023+
else
1024+
microsecond = rem(total, 1_000_000)
1025+
precision = precision_for_unit(unit)
1026+
{{year, month, day}, {hour, minute, second}} =
1027+
:calendar.gregorian_seconds_to_datetime(@unix_epoch + div(total, 1_000_000))
1028+
1029+
{:ok, %DateTime{year: year, month: month, day: day,
1030+
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision},
1031+
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
1032+
end
10201033
end
10211034

10221035
def precision_for_unit(unit) do
@@ -1034,6 +1047,13 @@ defmodule DateTime do
10341047
@doc """
10351048
Converts the given Unix time to DateTime.
10361049
1050+
The integer can be given in different unit
1051+
according to `System.convert_time_unit/3` and it will
1052+
be converted to microseconds internally.
1053+
1054+
Unix times are always in UTC and therefore the DateTime
1055+
will be returned in UTC.
1056+
10371057
## Examples
10381058
10391059
iex> DateTime.from_unix!(1464096368)
@@ -1046,6 +1066,15 @@ defmodule DateTime do
10461066
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
10471067
year: 2015, zone_abbr: "UTC"}
10481068
1069+
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
1070+
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
1071+
1072+
iex> DateTime.from_unix(-12345678910)
1073+
{:ok, %DateTime{calendar: Calendar.ISO, day: 13, hour: 4, microsecond: {0, 0}, minute: 44,
1074+
month: 10, second: 50, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
1075+
year: 1578, zone_abbr: "UTC"}}
1076+
1077+
When a Unix time before that moment is passed to `from_unix!/2`, an ArgumentError will be raised.
10491078
"""
10501079
@spec from_unix!(non_neg_integer, :native | System.time_unit) :: DateTime.t
10511080
def from_unix!(integer, unit \\ :seconds) when is_atom(unit) do
@@ -1057,7 +1086,7 @@ defmodule DateTime do
10571086
Converts the given DateTime to Unix time.
10581087
10591088
The DateTime is expected to be using the ISO calendar
1060-
with a year greater than or equal to 1970.
1089+
with a year greater than or equal to 0.
10611090
10621091
It will return the integer with the given unit,
10631092
according to `System.convert_time_unit/3`.
@@ -1073,11 +1102,19 @@ defmodule DateTime do
10731102
iex> DateTime.to_unix(dt)
10741103
1416517099
10751104
1105+
iex> flamel = %DateTime{calendar: Calendar.ISO, day: 22, hour: 8, microsecond: {527771, 6},
1106+
...> minute: 2, month: 3, second: 25, std_offset: 0, time_zone: "Etc/UTC",
1107+
...> utc_offset: 0, year: 1418, zone_abbr: "UTC"}
1108+
iex> DateTime.to_unix(flamel)
1109+
-17412508655
1110+
10761111
"""
10771112
@spec to_unix(DateTime.t, System.time_unit) :: non_neg_integer
1113+
def to_unix(datetime, unit \\ :seconds)
1114+
10781115
def to_unix(%DateTime{calendar: Calendar.ISO, std_offset: std_offset, utc_offset: utc_offset,
10791116
hour: hour, minute: minute, second: second, microsecond: {microsecond, _},
1080-
year: year, month: month, day: day}, unit \\ :seconds) when year >= 1970 do
1117+
year: year, month: month, day: day}, unit) when year >= 0 do
10811118
seconds =
10821119
:calendar.datetime_to_gregorian_seconds({{year, month, day}, {hour, minute, second}})
10831120
|> Kernel.-(utc_offset)

lib/elixir/test/elixir/calendar_test.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,25 @@ defmodule DateTimeTest do
6666
utc_offset: -12600, std_offset: 3600, time_zone: "Brazil/Manaus"}
6767
assert to_string(dt) == "2000-02-29 23:00:07-02:30 BRM Brazil/Manaus"
6868
end
69+
70+
test "from_unix/2 works with Unix times back to 0 Gregorian Seconds" do
71+
assert DateTime.from_unix(-62167219200) == {:ok,
72+
%DateTime{calendar: Calendar.ISO, day: 1, hour: 0, microsecond: {0, 0},
73+
minute: 0, month: 1, second: 0, std_offset: 0, time_zone: "Etc/UTC",
74+
utc_offset: 0, year: 0, zone_abbr: "UTC"}}
75+
76+
assert DateTime.from_unix(-62167219201) == :error
77+
end
78+
79+
test "to_unix/2 works with Unix times back to 0 Gregorian Seconds" do
80+
gregorian_0 = %DateTime{calendar: Calendar.ISO, day: 1, hour: 0, microsecond: {0, 0},
81+
minute: 0, month: 1, second: 0, std_offset: 0, time_zone: "Etc/UTC",
82+
utc_offset: 0, year: 0, zone_abbr: "UTC"}
83+
before_gregorian_0 = %DateTime{gregorian_0 | year: -1}
84+
85+
assert DateTime.to_unix(gregorian_0) == -62167219200
86+
assert_raise FunctionClauseError, fn ->
87+
DateTime.to_unix(before_gregorian_0)
88+
end
89+
end
6990
end

0 commit comments

Comments
 (0)