Skip to content

Commit 318b6a3

Browse files
Erweitere Z sigil um Week Ranges, Month Ranges & NaiveDateTime Intervalle.
1 parent d2d4d0f commit 318b6a3

File tree

6 files changed

+351
-18
lines changed

6 files changed

+351
-18
lines changed

lib/month.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ defmodule Shared.Month do
425425
iex> Month.range(@third_month_of_2018, @third_month_of_2019)
426426
%Shared.Month.Range{direction: :forward, size: 13, start: ~m[2018-03]}
427427
"""
428-
@spec range(Month.t(), Month.t()) :: Month.Range.t()
428+
@spec range(t(), t()) :: Month.Range.t()
429429
defdelegate range(start, stop), to: Shared.Month.Range, as: :new
430430

431431
@doc """

lib/week.ex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@ defmodule Shared.Week do
221221
Timex.from_iso_triplet({year, week, 1})
222222
end
223223

224+
@doc ~S"""
225+
Same as &Week.weekday(&1, :sunday)
226+
227+
## Examples
228+
229+
iex> Week.last_day(@third_week_of_2018)
230+
%Date{year: 2018, month: 1, day: 21}
231+
232+
"""
233+
@spec last_day(Shared.Week.t()) :: Date.t()
234+
def last_day(%__MODULE__{year: year, week: week}) do
235+
Timex.from_iso_triplet({year, week, 7})
236+
end
237+
224238
@doc ~S"""
225239
Returns the Date of a week's weekday.
226240
@@ -424,6 +438,28 @@ defmodule Shared.Week do
424438
Integer.floor_div(Date.diff(first_day(to_week), first_day(from_week)), 7)
425439
end
426440

441+
@doc """
442+
Creates a range of weeks from start to stop.
443+
444+
## Example
445+
446+
iex> Week.range(@third_week_of_2018, @third_week_of_2019)
447+
%Shared.Week.Range{direction: :forward, size: 53, start: ~v[2018-03]}
448+
"""
449+
@spec range(t(), t()) :: Week.Range.t()
450+
defdelegate range(start, stop), to: Shared.Week.Range, as: :new
451+
452+
@doc """
453+
Creates a range of weeks from start to stop in the given direction.
454+
455+
## Example
456+
457+
iex> Week.range(@third_week_of_2019, @third_week_of_2018, :backward)
458+
%Shared.Week.Range{direction: :backward, size: 53, start: ~v[2019-03]}
459+
"""
460+
@spec range(t(), t(), Shared.Week.Range.direction()) :: Shared.Week.Range.t()
461+
defdelegate range(start, stop, direction), to: Shared.Week.Range, as: :new
462+
427463
@doc ~S"""
428464
Returns the Week of the year or Date if a weekday is specified as well
429465

lib/week/range.ex

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
defmodule Shared.Week.Range do
2+
@moduledoc false
3+
alias Shared.Week
4+
5+
@type direction :: :forward | :backward
6+
@type t :: %__MODULE__{
7+
start: Week.t(),
8+
size: non_neg_integer(),
9+
direction: direction()
10+
}
11+
12+
defstruct [:start, :size, :direction]
13+
14+
@doc """
15+
Creates a Week Range with the given start Week and end Week or size.
16+
17+
## Examples
18+
19+
iex> Week.Range.forward(~v[2024-01], ~v[2024-03])
20+
%Week.Range{start: ~v[2024-01], size: 3, direction: :forward}
21+
22+
iex> Week.Range.forward(~v[2024-01], 2)
23+
%Week.Range{start: ~v[2024-01], size: 2, direction: :forward}
24+
"""
25+
@spec forward(Week.t(), Week.t() | (size :: pos_integer())) :: t()
26+
def forward(start, size_or_end), do: new(start, size_or_end, :forward)
27+
28+
@doc """
29+
Creates a Week Range with the given start week and end week or size.
30+
31+
## Examples
32+
33+
iex> Week.Range.backward(~v[2024-04], ~v[2024-02])
34+
%Week.Range{start: ~v[2024-04], size: 3, direction: :backward}
35+
36+
iex> Week.Range.backward(~v[2024-05], 2)
37+
%Week.Range{start: ~v[2024-05], size: 2, direction: :backward}
38+
"""
39+
@spec backward(Week.t(), Week.t() | (size :: pos_integer())) :: t()
40+
def backward(start, size_or_end), do: new(start, size_or_end, :backward)
41+
42+
@doc """
43+
Creates a Week Range with the given start Week and end Week or size.
44+
45+
## Examples
46+
47+
iex> Week.Range.forward(~v[2024-01], ~v[2024-03])
48+
%Week.Range{start: ~v[2024-01], size: 3, direction: :forward}
49+
50+
iex> Week.Range.forward(~v[2024-03], ~v[2024-01])
51+
%Week.Range{start: ~v[2024-03], size: 0, direction: :forward}
52+
53+
iex> Week.Range.forward(~v[2024-03], 2)
54+
%Week.Range{start: ~v[2024-03], size: 2, direction: :forward}
55+
"""
56+
@spec new(Week.t(), Week.t() | pos_integer()) :: t()
57+
def new(%Week{} = start, %Week{} = stop) do
58+
diff = Week.diff(start, stop) * -1
59+
direction = if diff < 0, do: :backward, else: :forward
60+
%__MODULE__{start: start, size: abs(diff) + 1, direction: direction}
61+
end
62+
63+
def new(%Week{} = start, size), do: new(start, size, :forward)
64+
65+
@spec new(Week.t(), pos_integer(), direction()) :: t()
66+
def new(_, _, direction) when direction not in [:forward, :backward],
67+
do: raise(ArgumentError, "Invalid direction: #{inspect(direction)}")
68+
69+
def new(%Week{} = start, %Week{} = stop, :forward) do
70+
size = max(Week.diff(start, stop) * -1 + 1, 0)
71+
%__MODULE__{start: start, size: size, direction: :forward}
72+
end
73+
74+
def new(%Week{} = start, %Week{} = stop, :backward) do
75+
size = max(Week.diff(stop, start) * -1 + 1, 0)
76+
%__MODULE__{start: start, size: size, direction: :backward}
77+
end
78+
79+
def new(_, size, _) when not is_integer(size) or size < 0,
80+
do: raise(ArgumentError, "Invalid size: #{inspect(size)}")
81+
82+
def new(%Week{} = start, size, direction),
83+
do: %__MODULE__{start: start, size: size, direction: direction}
84+
85+
@doc """
86+
Returns the earliest Week of the range.
87+
88+
Returns `nil` for empty ranges.
89+
90+
## Examples
91+
92+
iex> Week.Range.earliest(Week.Range.forward(~v[2024-01], ~v[2024-03]))
93+
~v[2024-01]
94+
95+
iex> Week.Range.earliest(Week.Range.backward(~v[2024-03], ~v[2024-02]))
96+
~v[2024-02]
97+
98+
iex> Week.Range.earliest(Week.Range.backward(~v[2024-01], ~v[2024-02]))
99+
nil
100+
"""
101+
def earliest(%__MODULE__{size: 0}), do: nil
102+
def earliest(%__MODULE__{start: start, direction: :forward}), do: start
103+
104+
def earliest(%__MODULE__{start: start, direction: :backward, size: size}),
105+
do: Week.shift(start, (size - 1) * -1)
106+
107+
@doc """
108+
Returns the latest week of the range.
109+
110+
Returns `nil` for empty ranges.
111+
112+
## Examples
113+
114+
iex> Week.Range.latest(Week.Range.forward(~v[2024-01], ~v[2024-03]))
115+
~v[2024-03]
116+
117+
iex> Week.Range.latest(Week.Range.backward(~v[2024-04], ~v[2024-02]))
118+
~v[2024-04]
119+
120+
iex> Week.Range.latest(Week.Range.backward(~v[2024-01], ~v[2024-02]))
121+
nil
122+
"""
123+
def latest(%__MODULE__{size: 0}), do: nil
124+
def latest(%__MODULE__{start: start, direction: :backward}), do: start
125+
126+
def latest(%__MODULE__{start: start, direction: :forward, size: size}),
127+
do: Week.shift(start, size - 1)
128+
129+
@doc """
130+
Converts Month Range to an end-inclusive Date.Range from earliest to latest Week.
131+
132+
The resulting date range will always be built with a step size of 1, but the
133+
range might be empty.
134+
135+
## Example
136+
137+
iex> Shared.Week.Range.forward(~v[2024-01], ~v[2024-03]) |> Shared.Week.Range.to_date_range()
138+
Date.range(~D[2024-01-01], ~D[2024-01-21], 1)
139+
140+
iex> Shared.Week.Range.backward(~v[2024-01], ~v[2024-03]) |> Shared.Week.Range.to_date_range()
141+
Date.range(~D[2024-01-01], ~D[2023-12-31], 1)
142+
"""
143+
@spec to_date_range(t()) :: Date.Range.t()
144+
def to_date_range(%__MODULE__{size: 0, start: start}),
145+
do: Date.range(Week.first_day(start), Date.add(Week.first_day(start), -1), 1)
146+
147+
def to_date_range(%__MODULE__{} = month_range) do
148+
first = month_range |> earliest() |> Week.first_day()
149+
last = month_range |> latest() |> Week.last_day()
150+
Date.range(first, last)
151+
end
152+
153+
defimpl Enumerable do
154+
alias Shared.Week
155+
156+
def count(%Week.Range{size: size}), do: {:ok, size}
157+
158+
def member?(%Week.Range{size: 0}, _), do: {:ok, false}
159+
def member?(%Week.Range{start: week, size: 1}, %Week{} = week), do: {:ok, true}
160+
161+
def member?(%Week.Range{} = range, %Week{} = week) do
162+
earliest = Week.Range.earliest(range)
163+
latest = Week.Range.latest(range)
164+
165+
{:ok,
166+
Week.compare(week, earliest) in [:eq, :gt] and
167+
Week.compare(week, latest) in [:eq, :lt]}
168+
end
169+
170+
def reduce(%Week.Range{start: start, size: size, direction: direction}, acc, fun) do
171+
step =
172+
case direction do
173+
:forward -> 1
174+
:backward -> -1
175+
end
176+
177+
start
178+
|> Stream.iterate(&Week.shift(&1, step))
179+
|> Enum.take(size)
180+
|> Enumerable.reduce(acc, fun)
181+
end
182+
183+
def slice(%Week.Range{start: start, size: size, direction: direction}) do
184+
{:ok, size,
185+
fn start_at, amount, step ->
186+
step =
187+
case direction do
188+
:forward -> step
189+
:backward -> step * -1
190+
end
191+
192+
start_at =
193+
case direction do
194+
:forward -> start_at
195+
:backward -> start_at * -1
196+
end
197+
198+
start
199+
|> Week.shift(start_at)
200+
|> Stream.iterate(&Week.shift(&1, step))
201+
|> Enum.take(amount)
202+
end}
203+
end
204+
end
205+
end

lib/week/range_test.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
defmodule Shared.Week.RangeTest do
2+
@moduledoc false
3+
use ExUnit.Case
4+
alias Shared.Week
5+
6+
import Week, only: [sigil_v: 2]
7+
8+
doctest(Week.Range)
9+
10+
describe "is enumerable" do
11+
test "can be countet" do
12+
assert 2778 = Enum.count(Week.Range.forward(~v[2024-01], ~v[2077-12]))
13+
end
14+
15+
test "can be converted to a list" do
16+
assert [~v[2024-01], ~v[2024-02]] ==
17+
Enum.to_list(Week.Range.forward(~v[2024-01], ~v[2024-02]))
18+
end
19+
20+
test "can be sliced" do
21+
assert [~v[2024-05], ~v[2024-06]] ==
22+
Enum.slice(Week.Range.forward(~v[2024-01], ~v[2024-12]), 4, 2)
23+
end
24+
end
25+
26+
describe "is supported by ZeitraumProtokoll" do
27+
alias Shared.Zeitraum
28+
29+
test "can be converted to intervall" do
30+
assert %{from: ~N[2024-01-01 00:00:00], until: ~N[2054-01-05 00:00:00]} =
31+
Zeitraum.als_intervall(Week.Range.forward(~v[2024-01], ~v[2054-01]))
32+
end
33+
end
34+
end

0 commit comments

Comments
 (0)