diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex index 9467b489b..875c72fe6 100644 --- a/lib/ex_doc/retriever.ex +++ b/lib/ex_doc/retriever.ex @@ -2,6 +2,8 @@ defmodule ExDoc.Retriever do # Functions to extract documentation information from modules. @moduledoc false + alias ExDoc.Utils.NaturalOrder + defmodule Error do @moduledoc false defexception [:message] @@ -128,10 +130,10 @@ defmodule ExDoc.Retriever do type: module_data.type, deprecated: metadata[:deprecated], function_groups: function_groups, - docs: Enum.sort_by(docs, &sort_key(&1.name, &1.arity)), + docs: natural_sort(docs, &"#{&1.name}/#{&1.arity}"), doc: moduledoc, doc_line: doc_line, - typespecs: Enum.sort_by(types, &{&1.name, &1.arity}), + typespecs: natural_sort(types, &"#{&1.name}/#{&1.arity}"), source_path: source_path, source_url: source_link(source, module_data.line), language: module_data.language, @@ -139,9 +141,13 @@ defmodule ExDoc.Retriever do } end - defp sort_key(name, arity) do - first = name |> Atom.to_charlist() |> hd() - {first in ?a..?z, name, arity} + defp natural_sort(enumerable, mapper) when is_function(mapper, 1) do + enumerable + |> Enum.sort_by(fn elem -> + elem + |> mapper.() + |> NaturalOrder.to_sortable_charlist() + end) end defp doc_ast(format, %{"en" => doc_content}, options) do @@ -201,7 +207,7 @@ defmodule ExDoc.Retriever do deprecated: metadata[:deprecated], doc: doc_ast, doc_line: doc_line, - defaults: Enum.sort_by(defaults, fn {name, arity} -> sort_key(name, arity) end), + defaults: natural_sort(defaults, fn {name, arity} -> "#{name}/#{arity}" end), signature: signature(signature), specs: function_data.specs, source_path: source.path, diff --git a/lib/ex_doc/utils/natural_order.ex b/lib/ex_doc/utils/natural_order.ex new file mode 100644 index 000000000..41e1a3b29 --- /dev/null +++ b/lib/ex_doc/utils/natural_order.ex @@ -0,0 +1,28 @@ +defmodule ExDoc.Utils.NaturalOrder do + @moduledoc false + + def to_sortable_charlist(string) do + string + |> :unicode.characters_to_nfkd_list() + |> make_sortable() + end + + @offset -1_000_000_000 + + # Numbers come first, so group and pad them with offset + defp make_sortable([digit | chars]) when digit in ?0..?9 do + {digits, chars} = Enum.split_while(chars, &(&1 in ?0..?9)) + [@offset + List.to_integer([digit | digits]) | make_sortable(chars)] + end + + # Then underscore + defp make_sortable([?_ | chars]), do: [?0 | make_sortable(chars)] + + # Then uppercased letters and lowercased letters + defp make_sortable([char | chars]) when char in ?a..?z do + [char - 31.5 | make_sortable(chars)] + end + + defp make_sortable([char | chars]), do: [char | make_sortable(chars)] + defp make_sortable([]), do: [] +end diff --git a/test/ex_doc/retriever_test.exs b/test/ex_doc/retriever_test.exs index 5d02a7cb9..ab646d8cc 100644 --- a/test/ex_doc/retriever_test.exs +++ b/test/ex_doc/retriever_test.exs @@ -155,4 +155,39 @@ defmodule ExDoc.RetrieverTest do assert a.id == "A" assert a_a.id == "A.A" end + + test "natural sorting", c do + elixirc(c, ~S""" + defmodule NaturallySorted do + @type type_b :: any() + @type type_B :: any() + @type type_A :: any() + @type type_a :: any() + + def function_b(), do: :ok + + def function_B(), do: :ok + + def function_A(), do: :ok + + def function_a(), do: :ok + + def function_A(arg), do: arg + + def function_a(arg), do: arg + end + """) + + [mod] = Retriever.docs_from_modules([NaturallySorted], %ExDoc.Config{}) + + [function_A_0, function_A_1, function_a_0, function_a_1, function_B_0, function_b_0] = + mod.docs + + assert function_A_0.id == "function_A/0" + assert function_A_1.id == "function_A/1" + assert function_a_0.id == "function_a/0" + assert function_a_1.id == "function_a/1" + assert function_B_0.id == "function_B/0" + assert function_b_0.id == "function_b/0" + end end