defmodule MapSorter do @moduledoc """ Sorts a list of maps per a list of sort specs. Also supports: - keywords - structs implementing the `Access` behaviour - nested maps, keywords or structs implementing the `Access` behaviour """ use PersistConfig alias __MODULE__.{Log, SortSpecs} @default_formatter Logger.default_formatter() @logger_env get_app_env(:file_only_logger, :logger, []) @doc """ Sorts `maps` per the given `sort_specs`. Examples of sort specs for flat data structures: ``` - implicit: [:dob, :name] - mixed: [:dob, desc: :name] - explicit: [asc: :dob, desc: :name] ``` Examples of sort specs with a `Date` key for flat data structures: ``` - implicit: [{:dob, Date}, :name] - mixed: [{:dob, Date}, desc: :name] - explicit: [asc: {:dob, Date}, desc: :name] ``` Examples of sort specs for nested data structures: ``` - implicit: [[:birth, :date], :name] - mixed : [[:birth, :date], desc: :name] - explicit: [asc: [:birth, :date], desc: :name] ``` Examples of sort specs with a `Date` key for nested data structures: ``` - implicit: [{[:birth, :date], Date}, :name] - mixed: [{[:birth, :date], Date}, desc: :name] - explicit: [asc: {[:birth, :date], Date}, desc: :name] ``` ## Examples iex> require MapSorter iex> people = [ ...> %{name: "Mike", likes: "movies" , dob: "1992-04-15"}, ...> %{name: "Mary", likes: "travels", dob: "1992-04-15"}, ...> %{name: "Bill", likes: "karate" , dob: "1977-08-28"}, ...> %{name: "Joe" , likes: "boxing" , dob: "1977-08-28"}, ...> %{name: "Jill", likes: "cooking", dob: "1976-09-28"} ...> ] iex> sort_specs = Tuple.to_list({:dob, {:desc, :likes}}) iex> sorted_people = %{ ...> explicit: MapSorter.sort(people, asc: :dob, desc: :likes), ...> mixed: MapSorter.sort(people, [:dob, desc: :likes]), ...> runtime: MapSorter.sort(people, sort_specs) ...> } iex> sorted_people.explicit == sorted_people.mixed and ...> sorted_people.explicit == sorted_people.runtime and ...> sorted_people.explicit [ %{name: "Jill", likes: "cooking", dob: "1976-09-28"}, %{name: "Bill", likes: "karate" , dob: "1977-08-28"}, %{name: "Joe" , likes: "boxing" , dob: "1977-08-28"}, %{name: "Mary", likes: "travels", dob: "1992-04-15"}, %{name: "Mike", likes: "movies" , dob: "1992-04-15"} ] """ defmacro sort(maps, sort_specs) do # To enforce logger configuration at compile time. # Otherwise logger will use default configuration. # dbg_handler_config("Before setting/adding logger handlers...", __CALLER__) :ok = :logger.set_handler_config(:default, :formatter, @default_formatter) for {:handler, id, :logger_std_h, config} <- @logger_env do case :logger.add_handler(id, :logger_std_h, config) do :ok -> :ok {:error, {:already_exist, ^id}} -> :ok end end # dbg_handler_config("After setting/adding logger handlers...", __CALLER__) specs = case sort_specs do specs when is_list(specs) -> # [asc: {:dob, {:__aliases__, [line: 7], [:Date]}}] => # [asc: {:dob, Date}] {specs, []} = Code.eval_quoted(specs) specs specs -> # In case any module attributes... Macro.expand(specs, __CALLER__) end :ok = Log.debug(:sort_specs, {sort_specs, specs, __ENV__, __CALLER__}) case SortSpecs.to_quoted(specs) do {:ok, fun_ast} -> quote do: Enum.sort(unquote(maps), unquote(fun_ast)) {:error, invalid_specs} -> :ok = Log.warning(:invalid_specs, {invalid_specs, __ENV__, __CALLER__}) maps end end ## Private functions # defp get_handler_config(id, color) do # case :logger.get_handler_config(id) do # {:ok, %{config: %{type: :file, file: file}, level: _level}} -> # file # {:ok, # %{ # config: %{type: :standard_io}, # level: level, # formatter: {Logger.Formatter, %Logger.Formatter{colors: colors}} # }} -> # {id, level, colors[color]} # {:error, {:not_found, ^id} = error} -> # error # end # end # @sep "===========================================================" # @yellow "\e[33m" # @light_yellow "\e[93m" # @reset "\e[0m" # defp dbg_handler_config(msg, env) do # """ # #{@yellow}#{@sep} # #{@light_yellow}#{msg} # #{inspect(env.module)}.#{inspect(env.function)}:#{env.line} # #{get_handler_config(:debug_handler, :debug) |> inspect()} # #{get_handler_config(:default, :debug) |> inspect()} # #{@yellow}#{@sep}#{@reset} # """ # |> IO.puts() # end end