From b70b46f77836b5e3f83300d9ba046b0ae973dca3 Mon Sep 17 00:00:00 2001 From: pnezis Date: Mon, 13 Jul 2026 00:56:02 +0300 Subject: [PATCH] Fix `Enum.min/2,3` and `Enum.max/2,3` with a custom sorter on ranges The range clause of `aggregate/3` only compared the two endpoints of the range, which is correct solely for sorters consistent with the natural integer order. Any other comparator returned a different result than the identical call on the equivalent list or stream: sorter = fn a, b -> rem(a, 5) >= rem(b, 5) end Enum.max(1..10, sorter) #=> 1, expected 4 Enum.max(Enum.to_list(1..10), sorter) #=> 4 Once the range is known to be non-empty, take the endpoint shortcut only when the sorter is one of the defaults (`&>=/2` or `&<=/2`, both order-consistent); any other sorter traverses the elements through `reduce_range/5`, seeded with the first element. --- lib/elixir/lib/enum.ex | 23 ++++++++++++++++++----- lib/elixir/test/elixir/enum_test.exs | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 3ca9a3f3d4b..f5af8e51346 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -4309,11 +4309,24 @@ defmodule Enum do empty.() _ -> - last = last - rem(last - first, step) - - case fun.(first, last) do - true -> first - false -> last + # The endpoint shortcut is only valid for sorters consistent with + # the natural integer order of the range elements, which is known + # to hold for the default sorters; any other sorter traverses the + # elements, seeded with the first one since the range is not empty + if fun == (&<=/2) or fun == (&>=/2) do + last = last - rem(last - first, step) + + case fun.(first, last) do + true -> first + false -> last + end + else + reduce_range(first + step, last, step, first, fn element, acc -> + case fun.(acc, element) do + true -> acc + false -> element + end + end) end end end diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 1be9e4a2eee..b001776757f 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -2096,6 +2096,14 @@ defmodule EnumTest.Range do assert Enum.max(1..2, fn -> 0 end) === 2 end + test "max/2 with custom sorter" do + sorter = fn a, b -> rem(a, 5) >= rem(b, 5) end + assert Enum.max(1..10, sorter) == 4 + assert Enum.max(1..10//2, sorter) == 9 + assert Enum.max(10..1//-1, sorter) == 9 + assert Enum.max(1..0//1, sorter, fn -> :empty end) == :empty + end + test "max_by/2" do assert Enum.max_by(1..1, fn x -> :math.pow(-2, x) end) == 1 assert Enum.max_by(1..3, fn x -> :math.pow(-2, x) end) == 2 @@ -2139,6 +2147,14 @@ defmodule EnumTest.Range do assert Enum.min(1..2, fn -> 0 end) === 1 end + test "min/2 with custom sorter" do + sorter = fn a, b -> rem(a, 5) <= rem(b, 5) end + assert Enum.min(1..10, sorter) == 5 + assert Enum.min(1..10//2, sorter) == 5 + assert Enum.min(10..1//-1, sorter) == 10 + assert Enum.min(1..0//1, sorter, fn -> :empty end) == :empty + end + test "min_by/2" do assert Enum.min_by(1..1, fn x -> :math.pow(-2, x) end) == 1 assert Enum.min_by(1..3, fn x -> :math.pow(-2, x) end) == 3