Current section
Files
Jump to
Current section
Files
lib/natural_sort.ex
defmodule NaturalSort do
@moduledoc """
Sort a list naturally.
Sort functions will not, generally, sort strings containing
numbers the same way a human would.
Given a list:
["a10", "a05c", "a1", "a", "a2", "a1a", "a0", "a1b", "a20"]
Applying standard sort will produce
["a", "a0", "a05c", "a1", "a10", "a1a", "a1b", "a2", "a20"]
But applying a natural sort will give:
["a", "a0", "a1", "a1a", "a1b", "a2", "a05c", "a10", "a20"]
## Prior art:
[VersionEye's naturalsorter gem](https://github.com/versioneye/naturalsorter)
was the inspiration, with that being based on
[Martin Pool's natural sorting algorithm](http://sourcefrog.net/projects/natsort/),
making direct use of the Ruby implementation of the original
C version.
Elixir's [Version](https://github.com/elixir-lang/elixir/blob/v1.0.4/lib/elixir/lib/version.ex#L1) module does a similar thing.
"""
@doc """
Sorts a list of strings. This works by leveraging Elixir's
`Enum.sort_by/3` function, which takes as the second argument
a mapping function. Each string is converted into a list
of strings and integers. Once in this form, applying the
sort function results in a correctly sorted list.
## Options
There are currently two available options (passed as a
keyword list), `:direction` and `case_sensitive`.
* `:direction` may have a value of `:asc` or `:desc`, and
defaults to `:asc`.
* `:case_sensitive` may be `true` or `false`, and defaults
to `false`.
## Examples
iex> NaturalSort.sort(["x2-y7", "x8-y8", "x2-y08", "x2-g8" ])
["x2-g8", "x2-y7", "x2-y08", "x8-y8" ]
iex> NaturalSort.sort(["a5", "a400", "a1"], direction: :desc)
["a400", "a5", "a1"]
iex> NaturalSort.sort(["foo03.z", "foo45.D", "foo06.a", "foo06.A", "foo"], case_sensitive: :true)
["foo", "foo03.z", "foo06.A", "foo06.a", "foo45.D"]
iex> NaturalSort.sort(["foo03.z", "foo45.D", "foo06.a", "foo06.A", "foo"], [case_sensitive: :true, direction: :desc])
["foo45.D", "foo06.a", "foo06.A", "foo03.z", "foo"]
"""
def sort([]), do: []
def sort(list, options \\ []) do
direction = Keyword.get(options, :direction, :asc)
case_sensitive? = Keyword.get(options, :case_sensitive, false)
Enum.sort_by(list,
fn x -> format_item(x, case_sensitive?) end,
sort_direction(direction))
end
##################################################
# String -> List formatter
defp format_item(item, case_sensitive?) when is_number(item), do: item
defp format_item(item, case_sensitive?) do
item
|> format_case(case_sensitive?)
# NOTE uses [relatively slow] unicode flag in regex.
|> string_scan(~r/(\p{Nd}+)|(\p{L}+)/u)
|> List.flatten
|> Enum.map(fn item -> convert_integers(item) end)
end
defp convert_integers(string) do
# NOTE uses [relatively slow] unicode flag in regex.
case Regex.match?(~r/\p{Nd}+/u, string) do
true -> String.to_integer(string)
false -> string
end
end
defp string_scan(string, regex), do: Regex.scan(regex, string, capture: :all_but_first)
##################################################
# Options
defp sort_direction(dir) do
case dir do
:asc -> &<=/2
:desc -> &>=/2
end
end
defp format_case(item, case_sensitive?) do
case case_sensitive? do
true -> item
false -> String.downcase(item)
end
end
##################################################
end