Skip to content

Commit

Permalink
Support negative dates in Calendar (elixir-lang#6638)
Browse files Browse the repository at this point in the history
  • Loading branch information
lexmag authored Feb 11, 2018
1 parent 179566d commit 2d4be98
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 30 deletions.
17 changes: 15 additions & 2 deletions lib/elixir/lib/calendar/date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ defmodule Date do
"2000-02-28"
iex> Date.to_string(~N[2000-02-28 01:23:45])
"2000-02-28"
iex> Date.to_string(~D[-0100-12-15])
"-0100-12-15"
"""
@spec to_string(Calendar.date()) :: String.t()
Expand Down Expand Up @@ -254,6 +256,12 @@ defmodule Date do
end
end

def from_iso8601(<<?-, next, rest::binary>>, calendar) when next != ?- do
with {:ok, %{year: year} = date} <- from_iso8601(<<next>> <> rest, calendar) do
{:ok, %{date | year: -year}}
end
end

def from_iso8601(<<_::binary>>, _calendar) do
{:error, :invalid_format}
end
Expand All @@ -270,6 +278,7 @@ defmodule Date do
~D[2015-01-23]
iex> Date.from_iso8601!("2015:01:23")
** (ArgumentError) cannot parse "2015:01:23" as date, reason: :invalid_format
"""
@spec from_iso8601!(String.t(), Calendar.calendar()) :: t
def from_iso8601!(string, calendar \\ Calendar.ISO) do
Expand Down Expand Up @@ -517,9 +526,10 @@ defmodule Date do
~D[2000-01-01]
iex> Date.add(~D[2000-01-01], 2)
~D[2000-01-03]
iex> Date.add(~N[2000-01-01 09:00:00], 2)
~D[2000-01-03]
iex> Date.add(~D[-0010-01-01], -2)
~D[-0011-12-30]
"""
@since "1.5.0"
Expand All @@ -542,7 +552,8 @@ defmodule Date do
2
iex> Date.diff(~D[2000-01-01], ~D[2000-01-03])
-2
iex> Date.diff(~D[0000-01-02], ~D[-0001-12-30])
3
iex> Date.diff(~D[2000-01-01], ~N[2000-01-03 09:00:00])
-2
Expand Down Expand Up @@ -601,6 +612,8 @@ defmodule Date do
2
iex> Date.day_of_week(~N[2016-11-01 01:23:45])
2
iex> Date.day_of_week(~D[-0015-10-30])
3
"""
@since "1.4.0"
Expand Down
6 changes: 6 additions & 0 deletions lib/elixir/lib/calendar/datetime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,12 @@ defmodule DateTime do
iex> DateTime.to_string(dt)
"2000-02-29 23:00:07-04:00 AMT America/Manaus"
iex> dt = %DateTime{year: -100, month: 12, day: 19, zone_abbr: "CET",
...> hour: 3, minute: 20, second: 31, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Stockholm"}
iex> DateTime.to_string(dt)
"-0100-12-19 03:20:31+01:00 CET Europe/Stockholm"
"""
@spec to_string(Calendar.datetime()) :: String.t()
def to_string(%{calendar: calendar} = datetime) do
Expand Down
70 changes: 55 additions & 15 deletions lib/elixir/lib/calendar/iso.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ defmodule Calendar.ISO do
@behaviour Calendar

@unix_epoch 62_167_219_200
unix_start = 1_000_000 * -@unix_epoch
unix_start = (315_537_897_600 + @unix_epoch) * -1_000_000
unix_end = 315_569_519_999_999_999 - @unix_epoch * 1_000_000
@unix_range_microseconds unix_start..unix_end

@type year :: 0..9999
@type year :: -9999..9999
@type month :: 1..12
@type day :: 1..31

Expand All @@ -45,6 +45,8 @@ defmodule Calendar.ISO do
{730485, {43200000000, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 13, 0, 0, {0, 6})
{730485, {46800000000, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(-1, 1, 1, 0, 0, 0, {0, 6})
{-365, {0, 86400000000}}
"""
@impl true
Expand Down Expand Up @@ -73,6 +75,8 @@ defmodule Calendar.ISO do
{2000, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
{2000, 1, 1, 12, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
{-1, 1, 1, 0, 0, 0, {0, 6}}
"""
@spec naive_datetime_from_iso_days(Calendar.iso_days()) :: {
Expand Down Expand Up @@ -159,7 +163,7 @@ defmodule Calendar.ISO do
719_528
end

def date_to_iso_days(year, month, day) when year in 0..9999 do
def date_to_iso_days(year, month, day) when year in -9999..9999 do
true = day <= days_in_month(year, month)

days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
Expand All @@ -176,10 +180,24 @@ defmodule Calendar.ISO do
{year, month, day_in_month + 1}
end

def date_from_iso_days(days) when days in -3_652_059..-1 do
{year, day_of_year} = days_to_year(-days)
previous_extra_day = if leap_year?(year), do: 1, else: 0
extra_day = if leap_year?(year + 1), do: 1, else: 0
day_of_year = @days_per_nonleap_year + extra_day - day_of_year
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
{-year - 1, month, day_in_month + previous_extra_day}
end

defp div_mod(int1, int2) do
div = div(int1, int2)
mod = int1 - div * int2
{div, mod}
rem = int1 - div * int2

if rem >= 0 do
{div, rem}
else
{div - 1, rem + int2}
end
end

@doc """
Expand All @@ -199,6 +217,8 @@ defmodule Calendar.ISO do
29
iex> Calendar.ISO.days_in_month(2004, 4)
30
iex> Calendar.ISO.days_in_month(-1, 5)
31
"""
@spec days_in_month(year, month) :: 28..31
Expand All @@ -225,12 +245,14 @@ defmodule Calendar.ISO do
true
iex> Calendar.ISO.leap_year?(1900)
false
iex> Calendar.ISO.leap_year?(-4)
true
"""
@spec leap_year?(year) :: boolean()
@impl true
def leap_year?(year) when is_integer(year) and year >= 0 do
rem(year, 4) === 0 and (rem(year, 100) > 0 or rem(year, 400) === 0)
def leap_year?(year) when is_integer(year) do
rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0)
end

@doc """
Expand All @@ -242,18 +264,21 @@ defmodule Calendar.ISO do
iex> Calendar.ISO.day_of_week(2016, 10, 31)
1
iex> Calendar.ISO.day_of_week(2016, 11, 01)
iex> Calendar.ISO.day_of_week(2016, 11, 1)
2
iex> Calendar.ISO.day_of_week(2016, 11, 02)
iex> Calendar.ISO.day_of_week(2016, 11, 2)
3
iex> Calendar.ISO.day_of_week(2016, 11, 03)
iex> Calendar.ISO.day_of_week(2016, 11, 3)
4
iex> Calendar.ISO.day_of_week(2016, 11, 04)
iex> Calendar.ISO.day_of_week(2016, 11, 4)
5
iex> Calendar.ISO.day_of_week(2016, 11, 05)
iex> Calendar.ISO.day_of_week(2016, 11, 5)
6
iex> Calendar.ISO.day_of_week(2016, 11, 06)
iex> Calendar.ISO.day_of_week(2016, 11, 6)
7
iex> Calendar.ISO.day_of_week(-99, 1, 31)
4
"""
@spec day_of_week(year, month, day) :: 1..7
@impl true
Expand All @@ -273,6 +298,7 @@ defmodule Calendar.ISO do
"02:02:02.00"
iex> Calendar.ISO.time_to_string(2, 2, 2, {2, 0})
"02:02:02"
"""
@spec time_to_string(
Calendar.hour(),
Expand Down Expand Up @@ -311,6 +337,9 @@ defmodule Calendar.ISO do
"2015-02-28"
iex> Calendar.ISO.date_to_string(2017, 8, 1)
"2017-08-01"
iex> Calendar.ISO.date_to_string(-99, 1, 31)
"-0099-01-31"
"""
@spec date_to_string(year, month, day) :: String.t()
@impl true
Expand All @@ -335,6 +364,7 @@ defmodule Calendar.ISO do
"2015-02-28 01:02:03.000004"
iex> Calendar.ISO.naive_datetime_to_string(2017, 8, 1, 1, 2, 3, {4, 5})
"2017-08-01 01:02:03.00000"
"""
@impl true
@spec naive_datetime_to_string(
Expand Down Expand Up @@ -363,6 +393,7 @@ defmodule Calendar.ISO do
"2015-02-28 01:02:03.00000-08:00 PST America/Los_Angeles"
iex> Calendar.ISO.datetime_to_string(2015, 2, 28, 1, 2, 3, {4, 5}, "America/Los_Angeles", "PDT", -28800, 3600)
"2015-02-28 01:02:03.00000-07:00 PDT America/Los_Angeles"
"""
@impl true
@spec datetime_to_string(
Expand Down Expand Up @@ -407,13 +438,17 @@ defmodule Calendar.ISO do
true
iex> Calendar.ISO.valid_date?(2015, 2, 30)
false
iex> Calendar.ISO.valid_date?(-1, 12, 31)
true
iex> Calendar.ISO.valid_date?(-1, 12, 32)
false
"""
@impl true
@since "1.5.0"
@spec valid_date?(year, month, day) :: boolean
def valid_date?(year, month, day) do
month in 1..12 and year in 0..9999 and day in 1..days_in_month(year, month)
month in 1..12 and year in -9999..9999 and day in 1..days_in_month(year, month)
end

@doc """
Expand All @@ -429,6 +464,7 @@ defmodule Calendar.ISO do
true
iex> Calendar.ISO.valid_time?(24, 0, 0, {0, 0})
false
"""
@impl true
@since "1.5.0"
Expand Down Expand Up @@ -474,11 +510,15 @@ defmodule Calendar.ISO do
defp sign(total) when total < 0, do: "-"
defp sign(_), do: "+"

defp zero_pad(val, count) do
defp zero_pad(val, count) when val >= 0 do
num = Integer.to_string(val)
:binary.copy("0", count - byte_size(num)) <> num
end

defp zero_pad(val, count) do
"-" <> zero_pad(-val, count)
end

## Helpers

@doc false
Expand Down
14 changes: 13 additions & 1 deletion lib/elixir/lib/calendar/naive_datetime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ defmodule NaiveDateTime do
21
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:10], ~N[2014-10-02 00:29:12])
-2
iex> NaiveDateTime.diff(~N[-0001-10-02 00:29:10], ~N[-0001-10-02 00:29:12])
-2
# to Gregorian seconds
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:10], ~N[0000-01-01 00:00:00])
Expand Down Expand Up @@ -371,6 +373,8 @@ defmodule NaiveDateTime do
"2000-02-28 23:00:13"
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13.001])
"2000-02-28 23:00:13.001"
iex> NaiveDateTime.to_string(~N[-0100-12-15 03:20:31])
"-0100-12-15 03:20:31"
This function can also be used to convert a DateTime to a string without
the time zone information:
Expand Down Expand Up @@ -457,7 +461,15 @@ defmodule NaiveDateTime do
"""
@spec from_iso8601(String.t(), Calendar.calendar()) :: {:ok, t} | {:error, atom}
def from_iso8601(string, calendar \\ Calendar.ISO) when is_binary(string) do
def from_iso8601(string, calendar \\ Calendar.ISO)

def from_iso8601(<<?-, next, rest::binary>>, calendar) when next != ?- do
with {:ok, %{year: year} = naive_datetime} <- from_iso8601(<<next>> <> rest, calendar) do
{:ok, %{naive_datetime | year: -year}}
end
end

def from_iso8601(string, calendar) when is_binary(string) do
with <<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep, rest::binary>> <- string,
true <- sep in [?\s, ?T],
<<hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>> <- rest,
Expand Down
Loading

0 comments on commit 2d4be98

Please sign in to comment.