Skip to content

Commit 8c086e1

Browse files
uesteibarjosevalim
authored andcommitted
Add Date.range/2 (#6227)
1 parent 85352d3 commit 8c086e1

File tree

3 files changed

+173
-0
lines changed

3 files changed

+173
-0
lines changed

lib/elixir/lib/calendar/date.ex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,31 @@ defmodule Date do
5656
@type t :: %Date{year: Calendar.year, month: Calendar.month,
5757
day: Calendar.day, calendar: Calendar.calendar}
5858

59+
@doc """
60+
Returns a Date.Range
61+
62+
## Examples
63+
64+
iex> Date.range(~D[2000-01-01], ~D[2001-01-01])
65+
#DateRange<~D[2000-01-01], ~D[2001-01-01]>
66+
67+
"""
68+
69+
@spec range(Date.t, Date.t) :: Date.Range.t
70+
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
71+
%Date.Range{
72+
first: first,
73+
last: last,
74+
first_rata_die: to_rata_die_days(first),
75+
last_rata_die: to_rata_die_days(last),
76+
}
77+
end
78+
79+
def range(%Date{}, %Date{}) do
80+
raise ArgumentError,
81+
"both dates must have matching calendars"
82+
end
83+
5984
@doc """
6085
Returns the current date in UTC.
6186
@@ -474,6 +499,12 @@ defmodule Date do
474499
calendar.naive_datetime_to_rata_die(year, month, day, 0, 0, 0, {0, 0})
475500
end
476501

502+
@doc false
503+
def to_rata_die_days(date) do
504+
{days, _} = to_rata_die(date)
505+
days
506+
end
507+
477508
defp from_rata_die({days, _}, Calendar.ISO) do
478509
{year, month, day} = Calendar.ISO.date_from_rata_die_days(days)
479510
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
@@ -483,6 +514,11 @@ defmodule Date do
483514
%Date{year: year, month: month, day: day, calendar: target_calendar}
484515
end
485516

517+
@doc false
518+
def from_rata_die_days(rata_die, target_calendar) do
519+
from_rata_die({rata_die, {0, 86400000000}}, target_calendar)
520+
end
521+
486522
@doc """
487523
Calculates the day of the week of a given `Date` struct.
488524

lib/elixir/lib/calendar/date_range.ex

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
defmodule Date.Range do
2+
@moduledoc """
3+
Defines a range of dates.
4+
5+
A range of dates represents a discrete number of dates where
6+
the first and last values are dates with matching calendars.
7+
8+
Ranges of dates can be either increasing (`first <= last`) or
9+
decreasing (`first > last`). They are also always inclusive.
10+
11+
A range of dates implements the `Enumerable` protocol, which means
12+
functions in the `Enum` module can be used to work with
13+
ranges:
14+
15+
iex> range = Date.range(~D[2001-01-01], ~D[2002-01-01])
16+
iex> Enum.count(range)
17+
366
18+
iex> Enum.member?(range, ~D[2001-02-01])
19+
true
20+
iex> Enum.reduce(range, 0, fn(_date, acc) -> acc - 1 end)
21+
-366
22+
23+
"""
24+
25+
@opaque t :: %__MODULE__{first: Date.t, last: Date.t}
26+
@doc false
27+
defstruct [:first, :last, :first_rata_die, :last_rata_die]
28+
29+
defimpl Enumerable do
30+
def member?(%Date.Range{first: %{calendar: calendar, year: first_year, month: first_month, day: first_day},
31+
last: %{calendar: calendar, year: last_year, month: last_month, day: last_day},
32+
first_rata_die: first_rata_die, last_rata_die: last_rata_die},
33+
%Date{calendar: calendar, year: year, month: month, day: day}) do
34+
first = {first_year, first_month, first_day}
35+
last = {last_year, last_month, last_day}
36+
date = {year, month, day}
37+
38+
if first_rata_die <= last_rata_die do
39+
{:ok, date >= first and date <= last}
40+
else
41+
{:ok, date >= last and date <= first}
42+
end
43+
end
44+
45+
def member?(_, _) do
46+
{:ok, false}
47+
end
48+
49+
def count(%Date.Range{first_rata_die: first_rata_die, last_rata_die: last_rata_die}) do
50+
{:ok, abs(first_rata_die - last_rata_die) + 1}
51+
end
52+
53+
def reduce(%Date.Range{first_rata_die: first_rata_die, last_rata_die: last_rata_die, first: %{calendar: c}}, acc, fun) do
54+
reduce(first_rata_die, last_rata_die, acc, &(fun.(Date.from_rata_die_days(&1, c), &2)), first_rata_die <= last_rata_die)
55+
end
56+
57+
defp reduce(_x, _y, {:halt, acc}, _fun, _up?) do
58+
{:halted, acc}
59+
end
60+
61+
defp reduce(x, y, {:suspend, acc}, fun, up?) do
62+
{:suspended, acc, &reduce(x, y, &1, fun, up?)}
63+
end
64+
65+
defp reduce(x, y, {:cont, acc}, fun, _up? = true) when x <= y do
66+
reduce(x + 1, y, fun.(x, acc), fun, _up? = true)
67+
end
68+
69+
defp reduce(x, y, {:cont, acc}, fun, _up? = false) when x >= y do
70+
reduce(x - 1, y, fun.(x, acc), fun, _up? = false)
71+
end
72+
73+
defp reduce(_, _, {:cont, acc}, _fun, _up) do
74+
{:done, acc}
75+
end
76+
end
77+
78+
defimpl Inspect do
79+
def inspect(%Date.Range{first: first, last: last}, _) do
80+
"#DateRange<" <> inspect(first) <> ", " <> inspect(last) <> ">"
81+
end
82+
end
83+
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Code.require_file "../test_helper.exs", __DIR__
2+
Code.require_file "../fixtures/calendar/julian.exs", __DIR__
3+
4+
defmodule Date.RangeTest do
5+
use ExUnit.Case, async: true
6+
doctest Date.Range
7+
8+
setup do
9+
{:ok, range: Date.range(~D[2000-01-01], ~D[2001-01-01])}
10+
end
11+
12+
describe "Enum.member?/2" do
13+
test "for ascending range", %{range: range} do
14+
assert Enum.member?(range, ~D[2000-02-22])
15+
refute Enum.member?(range, ~D[2002-01-01])
16+
end
17+
18+
test "for descending range", %{range: range} do
19+
assert Enum.member?(range, ~D[2000-02-22])
20+
refute Enum.member?(range, ~D[1999-01-01])
21+
end
22+
end
23+
24+
describe "Enum.count/1" do
25+
test "counts days in range", %{range: range} do
26+
assert Enum.count(range) == 367
27+
end
28+
end
29+
30+
describe "Enum.reduce/3" do
31+
test "acts as a normal reduce" do
32+
range = Date.range(~D[2000-01-01], ~D[2000-01-03])
33+
fun = fn (date, acc) -> acc ++ [date] end
34+
35+
assert Enum.reduce(range, [], fun) == [~D[2000-01-01], ~D[2000-01-02], ~D[2000-01-03]]
36+
end
37+
end
38+
39+
test "both dates must have matching calendars" do
40+
first = ~D[2000-01-01]
41+
last = Calendar.Julian.date(2001, 01, 01)
42+
43+
assert_raise ArgumentError, "both dates must have matching calendars", fn ->
44+
Date.range(first, last)
45+
end
46+
end
47+
48+
test "accepts equal but not Calendar.ISO calendars" do
49+
first = Calendar.Julian.date(2001, 01, 01)
50+
last = Calendar.Julian.date(2000, 01, 01)
51+
52+
assert Date.range(first, last)
53+
end
54+
end

0 commit comments

Comments
 (0)