Skip to content

Commit d270e1f

Browse files
Improve Month/Week API consistency
API additions: - Add Month.parse!/1 for parity with Week.parse!/1 - Add Month.after?/2 for parity with Week.after?/2 - Add Month.before?/2 for parity with Week.before?/2 - Add Month.shift/2 for parity with Week.shift/2 - Add Month.to_datetime_interval/1 for parity with Week API fixes: - Rename InvalidMonthIndex to InvalidMonthIndexError - Fix Month.diff/2 argument order to match Week.diff/2 (to, from) Deprecations: - Month.add/2 -> use shift/2 - Month.earlier_than?/2 -> use before?/2 - Week.earlier_than?/2 -> use before?/2 Cleanup: - Remove unused to_week_sigils/1 from Zeitraum Co-authored-by: David Potschka <david.potschka@jobvalley.com> Co-authored-by: Joern Lang <joern.lang@jobvalley.com> Co-authored-by: Ona <no-reply@ona.com> Co-authored-by: Ona <no-reply@ona.com>
1 parent 4b8ee0a commit d270e1f

File tree

5 files changed

+177
-93
lines changed

5 files changed

+177
-93
lines changed

lib/month.ex

Lines changed: 136 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule Shared.Month do
2-
defmodule InvalidMonthIndex do
2+
defmodule InvalidMonthIndexError do
33
defexception [:message]
44
end
55

@@ -48,7 +48,7 @@ defmodule Shared.Month do
4848
%Month{year: 2019, month: 7}
4949
5050
iex> Month.new!(2019, -7)
51-
** (Shared.Month.InvalidMonthIndex) Month must be an integer between 1 and 12, but was -7
51+
** (Shared.Month.InvalidMonthIndexError) Month must be an integer between 1 and 12, but was -7
5252
5353
"""
5454
def new!(year, month) do
@@ -57,7 +57,7 @@ defmodule Shared.Month do
5757
month
5858

5959
{:error, :invalid_month_index} ->
60-
raise InvalidMonthIndex,
60+
raise InvalidMonthIndexError,
6161
"Month must be an integer between 1 and 12, but was " <> inspect(month)
6262
end
6363
end
@@ -107,7 +107,7 @@ defmodule Shared.Month do
107107
%Month{year: 2018, month: 5}
108108
109109
iex> Month.from_day!(%Date{year: 2018, month: 13, day: 17})
110-
** (Shared.Month.InvalidMonthIndex) Month must be an integer between 1 and 12, but was 13
110+
** (Shared.Month.InvalidMonthIndexError) Month must be an integer between 1 and 12, but was 13
111111
112112
"""
113113
@spec from_day!(Date.t() | DateTime.t() | NaiveDateTime.t()) :: t()
@@ -261,6 +261,32 @@ defmodule Shared.Month do
261261

262262
def parse(_str), do: {:error, :invalid_month_format}
263263

264+
@doc ~S"""
265+
## Examples
266+
267+
iex> Month.parse!("2019-10")
268+
%Month{year: 2019, month: 10}
269+
270+
iex> Month.parse!("2019-13")
271+
** (Shared.Month.InvalidMonthIndexError) Invalid month index: 2019-13
272+
273+
iex> Month.parse!("foo")
274+
** (Shared.Month.InvalidMonthIndexError) Invalid month format: foo
275+
276+
"""
277+
def parse!(string) do
278+
case parse(string) do
279+
{:ok, month} ->
280+
month
281+
282+
{:error, :invalid_month_index} ->
283+
raise InvalidMonthIndexError, "Invalid month index: #{string}"
284+
285+
{:error, :invalid_month_format} ->
286+
raise InvalidMonthIndexError, "Invalid month format: #{string}"
287+
end
288+
end
289+
264290
@doc ~S"""
265291
## Examples
266292
@@ -297,6 +323,31 @@ defmodule Shared.Month do
297323
"""
298324
def to_range(%__MODULE__{} = month), do: Date.range(first_day(month), last_day(month))
299325

326+
@doc ~S"""
327+
Returns an end-exclusive Datetime Interval spanning the whole month.
328+
329+
## Examples
330+
331+
iex> Shared.Month.to_datetime_interval(@third_month_of_2018)
332+
%Timex.Interval{
333+
from: ~N[2018-03-01 00:00:00],
334+
left_open: false,
335+
right_open: true,
336+
step: [seconds: 1],
337+
until: ~N[2018-04-01 00:00:00]
338+
}
339+
340+
"""
341+
@spec to_datetime_interval(t()) :: Shared.Zeitperiode.t()
342+
def to_datetime_interval(%__MODULE__{} = month) do
343+
{first_day, last_day} = to_dates(month)
344+
345+
{:ok, from} = NaiveDateTime.new(first_day, ~T[00:00:00])
346+
{:ok, until} = NaiveDateTime.new(Date.add(last_day, 1), ~T[00:00:00])
347+
348+
Shared.Zeitperiode.new(from, until)
349+
end
350+
300351
@doc ~S"""
301352
## Examples
302353
@@ -329,36 +380,32 @@ defmodule Shared.Month do
329380
do: Date.new!(year, month, day)
330381

331382
@doc ~S"""
332-
## Examples
383+
Returns the month advanced by the provided number of months from the starting month.
333384
334-
iex> Month.add(@third_month_of_2018, 9)
335-
%Month{year: 2018, month: 12}
336-
337-
iex> Month.add(@third_month_of_2018, 10)
338-
%Month{year: 2019, month: 1}
385+
## Examples
339386
340-
iex> Month.add(@third_month_of_2018, 22)
341-
%Month{year: 2020, month: 1}
387+
iex> Month.shift(~m[2020-05], 2)
388+
%Month{year: 2020, month: 7}
342389
343-
iex> Month.add(@third_month_of_2018, -2)
344-
%Month{year: 2018, month: 1}
390+
iex> Month.shift(~m[2020-05], -5)
391+
%Month{year: 2019, month: 12}
345392
346-
iex> Month.add(@third_month_of_2018, -3)
347-
%Month{year: 2017, month: 12}
393+
iex> Month.shift(~m[2018-03], 9)
394+
%Month{year: 2018, month: 12}
348395
349-
iex> Month.add(@third_month_of_2018, -15)
350-
%Month{year: 2016, month: 12}
396+
iex> Month.shift(~m[2018-03], 10)
397+
%Month{year: 2019, month: 1}
351398
352-
iex> Month.add(@third_month_of_2018, 0)
399+
iex> Month.shift(~m[2018-03], 0)
353400
%Month{year: 2018, month: 3}
354401
355402
"""
356-
@spec add(t(), integer()) :: t()
357-
def add(%__MODULE__{} = month, 0), do: month
403+
@spec shift(t(), integer()) :: t()
404+
def shift(%__MODULE__{} = month, 0), do: month
358405

359-
def add(%__MODULE__{year: year, month: month}, months_to_add) when is_integer(months_to_add) do
360-
new_year = year + div(months_to_add, 12)
361-
new_month = month + rem(months_to_add, 12)
406+
def shift(%__MODULE__{year: year, month: month}, amount_of_months) when is_integer(amount_of_months) do
407+
new_year = year + div(amount_of_months, 12)
408+
new_month = month + rem(amount_of_months, 12)
362409

363410
cond do
364411
new_month > 12 -> new!(new_year + 1, new_month - 12)
@@ -367,46 +414,91 @@ defmodule Shared.Month do
367414
end
368415
end
369416

370-
@doc """
371-
Returns the number of months you need to add to first_month to arrive at second_month.
417+
@doc ~S"""
418+
Deprecated: Use `shift/2` instead.
372419
"""
373-
@spec diff(first_month :: t(), second_month :: t()) :: integer()
374-
def diff(%__MODULE__{year: first_year, month: first_month}, %__MODULE__{
375-
year: second_year,
376-
month: second_month
420+
@deprecated "Use shift/2 instead"
421+
@spec add(t(), integer()) :: t()
422+
def add(month, amount), do: shift(month, amount)
423+
424+
@doc ~S"""
425+
Returns the number of months from `from_month` to `to_month`.
426+
427+
## Examples
428+
429+
iex> Month.diff(~m[2025-02], ~m[2025-01])
430+
1
431+
432+
iex> Month.diff(~m[2025-01], ~m[2025-01])
433+
0
434+
435+
iex> Month.diff(~m[2025-01], ~m[2025-02])
436+
-1
437+
438+
iex> Month.diff(~m[2025-01], ~m[2024-01])
439+
12
440+
441+
"""
442+
@spec diff(to_month :: t(), from_month :: t()) :: integer()
443+
def diff(%__MODULE__{year: to_year, month: to_month}, %__MODULE__{
444+
year: from_year,
445+
month: from_month
377446
}) do
378-
12 * (second_year - first_year) + second_month - first_month
447+
12 * (to_year - from_year) + to_month - from_month
379448
end
380449

381450
@doc ~S"""
451+
Returns true if the first month is before the second month.
452+
382453
## Examples:
383454
384-
iex> @third_month_of_2018 |> Month.earlier_than?(@third_month_of_2019)
455+
iex> Month.before?(~m[2018-03], ~m[2019-03])
385456
true
386457
387-
iex> @third_month_of_2018 |> Month.earlier_than?(@third_month_of_2017)
458+
iex> Month.before?(~m[2018-03], ~m[2017-03])
388459
false
389460
390-
iex> @third_month_of_2018 |> Month.earlier_than?(@fourth_month_of_2018)
461+
iex> Month.before?(~m[2018-03], ~m[2018-04])
391462
true
392463
393-
iex> @third_month_of_2018 |> Month.earlier_than?(@second_month_of_2019)
394-
true
395-
396-
iex> @third_month_of_2018 |> Month.earlier_than?(@third_month_of_2018)
397-
false
398-
399-
iex> @third_month_of_2018 |> Month.earlier_than?(@second_month_of_2018)
464+
iex> Month.before?(~m[2018-03], ~m[2018-03])
400465
false
401466
402467
"""
403-
def earlier_than?(%__MODULE__{year: year, month: month}, %__MODULE__{
468+
@spec before?(t(), t()) :: boolean()
469+
def before?(%__MODULE__{year: year, month: month}, %__MODULE__{
404470
year: other_year,
405471
month: other_month
406472
}) do
407473
year < other_year || (year == other_year && month < other_month)
408474
end
409475

476+
@doc ~S"""
477+
Deprecated: Use `before?/2` instead.
478+
"""
479+
@deprecated "Use before?/2 instead"
480+
def earlier_than?(month, other_month), do: before?(month, other_month)
481+
482+
@doc ~S"""
483+
Returns true if the month `month` is after the month `other_month`.
484+
485+
## Examples:
486+
487+
iex> ~m[2025-03] |> Month.after?(~m[2025-01])
488+
true
489+
490+
iex> ~m[2024-12] |> Month.after?(~m[2025-01])
491+
false
492+
493+
iex> ~m[2025-01] |> Month.after?(~m[2025-01])
494+
false
495+
496+
"""
497+
@spec after?(t(), t()) :: boolean()
498+
def after?(%__MODULE__{} = month, %__MODULE__{} = other_month) do
499+
not equal_or_earlier_than?(month, other_month)
500+
end
501+
410502
@doc ~S"""
411503
## Examples:
412504
@@ -430,7 +522,7 @@ defmodule Shared.Month do
430522
431523
"""
432524
def equal_or_earlier_than?(%__MODULE__{} = month, %__MODULE__{} = other_month) do
433-
month == other_month || earlier_than?(month, other_month)
525+
month == other_month || before?(month, other_month)
434526
end
435527

436528
@doc ~S"""
@@ -448,7 +540,7 @@ defmodule Shared.Month do
448540
def compare(%__MODULE__{} = month, month), do: :eq
449541

450542
def compare(%__MODULE__{} = first, %__MODULE__{} = second) do
451-
if first |> earlier_than?(second) do
543+
if before?(first, second) do
452544
:lt
453545
else
454546
:gt

lib/month/range.ex

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ defmodule Shared.Month.Range do
5555
"""
5656
@spec new(Month.t(), Month.t() | pos_integer()) :: t()
5757
def new(%Month{} = start, %Month{} = stop) do
58-
diff = Month.diff(start, stop)
58+
diff = Month.diff(stop, start)
5959
direction = if diff < 0, do: :backward, else: :forward
6060
%__MODULE__{start: start, size: abs(diff) + 1, direction: direction}
6161
end
@@ -67,12 +67,12 @@ defmodule Shared.Month.Range do
6767
do: raise(ArgumentError, "Invalid direction: #{inspect(direction)}")
6868

6969
def new(%Month{} = start, %Month{} = stop, :forward) do
70-
size = max(Month.diff(start, stop) + 1, 0)
70+
size = max(Month.diff(stop, start) + 1, 0)
7171
%__MODULE__{start: start, size: size, direction: :forward}
7272
end
7373

7474
def new(%Month{} = start, %Month{} = stop, :backward) do
75-
size = max(Month.diff(stop, start) + 1, 0)
75+
size = max(Month.diff(start, stop) + 1, 0)
7676
%__MODULE__{start: start, size: size, direction: :backward}
7777
end
7878

@@ -102,7 +102,7 @@ defmodule Shared.Month.Range do
102102
def earliest(%__MODULE__{start: start, direction: :forward}), do: start
103103

104104
def earliest(%__MODULE__{start: start, direction: :backward, size: size}),
105-
do: Month.add(start, (size - 1) * -1)
105+
do: Month.shift(start, (size - 1) * -1)
106106

107107
@doc """
108108
Returns the latest month of the range.
@@ -124,7 +124,7 @@ defmodule Shared.Month.Range do
124124
def latest(%__MODULE__{start: start, direction: :backward}), do: start
125125

126126
def latest(%__MODULE__{start: start, direction: :forward, size: size}),
127-
do: Month.add(start, size - 1)
127+
do: Month.shift(start, size - 1)
128128

129129
@doc """
130130
Converts Month Range to an end-inclusive Date.Range from earliest to latest Month.
@@ -175,7 +175,7 @@ defmodule Shared.Month.Range do
175175
end
176176

177177
start
178-
|> Stream.iterate(&Month.add(&1, step))
178+
|> Stream.iterate(&Month.shift(&1, step))
179179
|> Enum.take(size)
180180
|> Enumerable.reduce(acc, fun)
181181
end
@@ -196,8 +196,8 @@ defmodule Shared.Month.Range do
196196
end
197197

198198
start
199-
|> Month.add(start_at)
200-
|> Stream.iterate(&Month.add(&1, step))
199+
|> Month.shift(start_at)
200+
|> Stream.iterate(&Month.shift(&1, step))
201201
|> Enum.take(amount)
202202
end}
203203
end

0 commit comments

Comments
 (0)