Skip to content

Commit cacdbf6

Browse files
authored
Add Enum.min_max sorter (#14690)
1 parent b5885a6 commit cacdbf6

File tree

2 files changed

+106
-16
lines changed

2 files changed

+106
-16
lines changed

lib/elixir/lib/enum.ex

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,28 +2143,66 @@ defmodule Enum do
21432143

21442144
@doc """
21452145
Returns a tuple with the minimal and the maximal elements in the
2146-
enumerable according to Erlang's term ordering.
2146+
enumerable.
21472147
2148-
If multiple elements are considered maximal or minimal, the first one
2149-
that was found is returned.
2150-
2151-
Calls the provided `empty_fallback` function and returns its value if
2152-
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
2148+
By default, the comparison is done with the `<` sorter function,
2149+
as the function must not return true for equal elements.
21532150
21542151
## Examples
21552152
21562153
iex> Enum.min_max([2, 3, 1])
21572154
{1, 3}
21582155
2156+
iex> Enum.min_max(["foo", "bar", "baz"])
2157+
{"bar", "foo"}
2158+
21592159
iex> Enum.min_max([], fn -> {nil, nil} end)
21602160
{nil, nil}
21612161
2162+
The fact this function uses Erlang's term ordering means that the
2163+
comparison is structural and not semantic. Therefore, if you want
2164+
to compare structs, most structs provide a "compare" function, such as
2165+
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
2166+
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
2167+
sorting function, Elixir will automatically use the `compare/2` function
2168+
of said module:
2169+
2170+
iex> dates = [
2171+
...> ~D[2019-01-01],
2172+
...> ~D[2020-01-01],
2173+
...> ~D[2018-01-01]
2174+
...> ]
2175+
iex> Enum.min_max(dates, Date)
2176+
{~D[2018-01-01], ~D[2020-01-01]}
2177+
2178+
You can also pass a custom sorting function:
2179+
2180+
iex> Enum.min_max([2, 3, 1], &>/2)
2181+
{3, 1}
2182+
2183+
Finally, if you don't want to raise on empty enumerables, you can pass
2184+
the empty fallback:
2185+
2186+
iex> Enum.min_max([], fn -> nil end)
2187+
nil
2188+
21622189
"""
2163-
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
2190+
@spec min_max(t, (element, element -> boolean) | module()) ::
2191+
{element, element} | empty_result
2192+
when empty_result: any
2193+
@spec min_max(
2194+
t,
2195+
(element, element -> boolean) | module(),
2196+
(-> empty_result)
2197+
) :: {element, element} | empty_result
21642198
when empty_result: any
2165-
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
21662199

2167-
def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
2200+
def min_max(enumerable) do
2201+
min_max(enumerable, fn -> raise Enum.EmptyError end)
2202+
end
2203+
2204+
def min_max(first..last//step = range, empty_fallback)
2205+
when is_function(empty_fallback, 0) do
21682206
case Range.size(range) do
21692207
0 ->
21702208
empty_fallback.()
@@ -2175,11 +2213,39 @@ defmodule Enum do
21752213
end
21762214
end
21772215

2178-
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
2216+
def min_max(enumerable, empty_fallback)
2217+
when is_function(empty_fallback, 0) do
2218+
min_max(enumerable, &</2, empty_fallback)
2219+
end
2220+
2221+
def min_max(enumerable, sorter) when is_atom(sorter) do
2222+
min_max(enumerable, min_max_sort_fun(sorter))
2223+
end
2224+
2225+
def min_max(enumerable, sorter) when is_function(sorter, 2) do
2226+
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
2227+
end
2228+
2229+
def min_max(enumerable, sorter, empty_fallback)
2230+
when is_atom(sorter) and is_function(empty_fallback, 0) do
2231+
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
2232+
end
2233+
2234+
def min_max(enumerable, sorter, empty_fallback)
2235+
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
21792236
first_fun = &[&1 | &1]
21802237

2181-
reduce_fun = fn entry, [min | max] ->
2182-
[Kernel.min(min, entry) | Kernel.max(max, entry)]
2238+
reduce_fun = fn entry, [min | max] = acc ->
2239+
cond do
2240+
sorter.(entry, min) ->
2241+
[entry | max]
2242+
2243+
sorter.(max, entry) ->
2244+
[min | entry]
2245+
2246+
true ->
2247+
acc
2248+
end
21832249
end
21842250

21852251
case reduce_by(enumerable, first_fun, reduce_fun) do
@@ -2200,8 +2266,8 @@ defmodule Enum do
22002266
Returns a tuple with the minimal and the maximal elements in the
22012267
enumerable as calculated by the given function.
22022268
2203-
If multiple elements are considered maximal or minimal, the first one
2204-
that was found is returned.
2269+
By default, the comparison is done with the `<` sorter function,
2270+
as the function must not return true for equal elements.
22052271
22062272
## Examples
22072273
@@ -2259,7 +2325,7 @@ defmodule Enum do
22592325

22602326
def min_max_by(enumerable, fun, sorter, empty_fallback)
22612327
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
2262-
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
2328+
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
22632329
end
22642330

22652331
def min_max_by(enumerable, fun, sorter, empty_fallback)
@@ -2290,7 +2356,7 @@ defmodule Enum do
22902356
end
22912357
end
22922358

2293-
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
2359+
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
22942360

22952361
@doc """
22962362
Splits the `enumerable` in two lists according to the given function `fun`.

lib/elixir/test/elixir/enum_test.exs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,30 @@ defmodule EnumTest do
743743
assert_runs_enumeration_only_once(&Enum.min_max(&1, fn -> nil end))
744744
end
745745

746+
test "min_max/3" do
747+
dates = [~D[2020-01-01], ~D[2019-01-01]]
748+
749+
assert Enum.min_max(dates, Date) ==
750+
{~D[2019-01-01], ~D[2020-01-01]}
751+
752+
assert Enum.min_max([~D[2000-01-01]], Date) ==
753+
{~D[2000-01-01], ~D[2000-01-01]}
754+
755+
assert Enum.min_max([3, 1, 2], &>/2, fn -> nil end) ==
756+
{3, 1}
757+
758+
assert Enum.min_max([], &>/2, fn -> {:no_min, :no_max} end) ==
759+
{:no_min, :no_max}
760+
761+
assert Enum.min_max(%{}, &>/2, fn -> {:no_min, :no_max} end) ==
762+
{:no_min, :no_max}
763+
764+
assert Enum.min_max(1..5, &>/2, fn -> {:no_min, :no_max} end) ==
765+
{5, 1}
766+
767+
assert_runs_enumeration_only_once(&Enum.min_max(&1, fn a, b -> a > b end, fn -> nil end))
768+
end
769+
746770
test "min_max_by/2" do
747771
assert Enum.min_max_by(["aaa", "a", "aa"], fn x -> String.length(x) end) == {"a", "aaa"}
748772

0 commit comments

Comments
 (0)