Ordering a Keyword List Based on List of Keys

I imagine there is an easy way to do this with Enum but I am struggling to piece it together.

I have a keyword list with keys in some random order. I would like to order the keyword list based on a list of keys that I have in the right order. How would I sort based on the keys?

keyword_list = [test: 23, test3: 928, test1: 24, test2: 12]
order_of_keys = [:test, :test1, :test2, :test3]
iex(1)> keyword_list = [test: 23, test3: 928, test1: 24, test2: 12]
[test: 23, test3: 928, test1: 24, test2: 12]
iex(2)> order_of_keys = [:test, :test1, :test2, :test3]
[:test, :test1, :test2, :test3]
iex(3)> index = fn list, item -> Enum.find_index(list, &Kernel.==(&1, item)) end
#Function<43.97283095/2 in :erl_eval.expr/5>
iex(4)> Enum.sort_by(keyword_list, fn {key, _} -> index.(order_of_keys, key) end)
[test: 23, test1: 24, test2: 12, test3: 928]
1 Like

Since list are ordered and you have one with the proper order, i’d go with a more direct version, like this. Note that i assume that the ordered list only contains available keys.

Enum.reduce(order_of_keys, [], &[{&1, Keyword.fetch!(keyword_list, &1)} | &2]) 
|> Enum.reverse()

Or with a comprehension

for k <- order_of_keys, do: {k, Keyword.fetch!(keyword_list, k)}
7 Likes

Thank you for showing with and without comprehension. I never thought to do it this way (just build a new keyword list based on the order key).

Anything that is internally based on Keyword.get or Keyword.fetch will silently discard any duplicated keys. At most you’d want to use Keyword.get_values but you’d have to reconstruct the individual entries.

Marcus’ suggestion will preserve duplicates correctly but could be optimized further with memoization of the indexing, which could loosely be something like order_of_keys |> Enum.with_index() |> Map.new(). Then sort_by switches to Map.fetch inside.

6 Likes

Thank you, in my particular application I know I have no duplicates the and keys are the same so it was pretty easy to use comprehension to build up from the ordered listed. However I can see in most applications the danger so its good to know how to do it safely as you suggest. Enum.with_index is awesome, I remember reading about it but haven’t had a use yet. Thanks for the suggestion!

keyword_list = [test: 23, test3: 928, test1: 24, test2: 12]
order_of_keys = [:test, :test1, :test2, :test3]
indexed_keys = order_of_keys |> Enum.with_index |> Map.new
Enum.sort_by(keyword_list, fn {key, _} -> Map.get(indexed_keys, key, :invalid) end)

This sorts by the requested order and keeps the current order for duplicate keys.

4 Likes
defmodule KW do
  def take_in_order(kw, keys)
  when is_list(kw) and is_list(keys) do
    keys
    |> Enum.reduce({[], kw}, fn key, {result, starting_kw} ->
      {values, next_kw} = Keyword.pop(starting_kw, key)
      {[{key, values} | result], next_kw}
    end)
    |> elem(0)
    |> Enum.reverse()
  end
end

This basically iterates over the keys and gradually prepends each key/value pair to the result – while gradually taking values from a constantly shrinking keyword list – until we finally take only the first element of the resulting accumulator tuple (the result in a reversed order) which we reverse to get the desired result.

This code assumes no duplicated keys (and you said your input will conform to this rule).

Example:

iex(1)> kw = [d: 4, c: 3, b: 2, a: 1]
[d: 4, c: 3, b: 2, a: 1]
iex(2)> KW.take_in_order(kw, [:a, :b, :c])
[a: 1, b: 2, c: 3]
3 Likes