Utility functions for for maps, keyword lists, and nested associative structures.
Summary
Functions
Recursively converts a map to a keyword list.
Recursively converts a keyword list to a map.
Recursively converts string map keys to atoms using the supplied function.
Deeply merges two maps, preferring the left value for non-map conflicts.
Deeply merges two maps, preferring the right value for non-map conflicts.
Deeply merges two maps, resolving non-map conflicts with a function.
Deletes the entry at a nested path in a map or keyword list.
Calls a function for each entry in a map or keyword list using breadth-first traversal.
Calls a function for each entry in a map or keyword list using depth-first traversal.
Returns whether a map and keyword list contain equivalent entries.
Updates values in a map or keyword list using a similarly-shaped evolution map.
Fetches the value at a nested path in a map or keyword list.
Fetches the value at a nested path in a map or keyword list, raising if absent.
Counts the maximum number of nested map or keyword-list levels below the root.
Gets the value at a nested path in a map or keyword list.
Returns each leaf value in a map or keyword list with the path needed to reach it.
Returns the values at several nested paths.
Returns whether a map or keyword list contains the given path.
Inverts a map by grouping original keys under their stringified values.
Merges two maps or two keyword lists, preferring values from the left input.
Merges two maps or two keyword lists, preferring values from the right input.
Returns whether a value at a nested path satisfies a predicate.
Keeps only the requested keys from each map or keyword list in a list. An analogue of the SQL
select statement.
Returns a function that gets a key from a map or keyword list.
Returns a function that keeps only the requested keys from a map or keyword list.
Puts a value at a nested path only when the full path is absent.
Puts a value at a nested path in a map or keyword list.
Renames keys in a map or keyword list.
Replaces the value at a nested path if the full path exists.
Replaces the value at a nested path, raising if the full path is absent.
Recursively converts map keys to strings.
Flattens nested map or keyword list keys into a single level.
Updates the value at a nested path in a map or keyword list.
Updates the value at a nested path, raising if the full path is absent.
Returns whether a map or keyword list satisfies every keyed predicate.
Returns whether any keyed predicate is satisfied by a map or keyword list.
Returns whether a map or keyword list contains equal values at all condition paths.
Functions
Recursively converts a map to a keyword list.
Entries are sorted by key before conversion. Nested maps are converted recursively; keyword-list values are left unchanged.
Examples
iex> Algo.Assoc.as_keyword_list(%{b: 2, a: 1})
[a: 1, b: 2]
iex> Algo.Assoc.as_keyword_list(%{a: %{b: 2}})
[a: [b: 2]]
iex> Algo.Assoc.as_keyword_list(%{})
[]
Recursively converts a keyword list to a map.
Maps are returned unchanged.
Duplicate keys collapse according to Map.put/3, so the last duplicate value
wins. Nested keyword lists are converted recursively; non-keyword lists are
left unchanged.
Examples
iex> Algo.Assoc.as_map(a: 1, b: [c: 2])
%{a: 1, b: %{c: 2}}
iex> Algo.Assoc.as_map(a: 1, a: 2)
%{a: 2}
iex> Algo.Assoc.as_map([])
%{}
iex> Algo.Assoc.as_map(%{a: [b: 1]})
%{a: [b: 1]}
iex> Algo.Assoc.as_map(a: [1, 2, 3])
%{a: [1, 2, 3]}
Recursively converts string map keys to atoms using the supplied function.
Nested maps are converted recursively. Other values, including keyword lists, are left unchanged.
Pass either &String.to_atom/1 or &String.to_existing_atom/1 as the second
argument. String.to_atom/1 creates atoms for keys that do not already exist,
which can exhaust the VM atom table when used with untrusted or unbounded
input. String.to_existing_atom/1 avoids creating new atoms and raises
ArgumentError when a string key does not already have an existing atom.
Examples
iex> Algo.Assoc.atomise_keys_with(%{"a" => 1, "b" => %{"c" => 2}}, &String.to_atom/1)
%{a: 1, b: %{c: 2}}
iex> Algo.Assoc.atomise_keys_with(%{}, &String.to_existing_atom/1)
%{}
Deeply merges two maps, preferring the left value for non-map conflicts.
Keys that exist in only one map are copied into the result. If a key exists in both maps and both values are maps, those maps are recursively merged. If a key exists in both maps and either value is not a map, the left value is used.
Examples
iex> Algo.Assoc.deep_merge_left(%{a: %{b: 1}, c: 2}, %{a: %{d: 3}, c: 4, e: 5})
%{a: %{b: 1, d: 3}, c: 2, e: 5}
iex> Algo.Assoc.deep_merge_left(%{a: 1}, %{a: %{b: 2}})
%{a: 1}
Deeply merges two maps, preferring the right value for non-map conflicts.
Keys that exist in only one map are copied into the result. If a key exists in both maps and both values are maps, those maps are recursively merged. If a key exists in both maps and either value is not a map, the right value is used.
Examples
iex> Algo.Assoc.deep_merge_right(%{a: %{b: 1}, c: 2, e: 5}, %{a: %{d: 3}, c: 4})
%{a: %{b: 1, d: 3}, c: 4, e: 5}
iex> Algo.Assoc.deep_merge_right(%{a: %{b: 2}}, %{a: 1})
%{a: 1}
Deeply merges two maps, resolving non-map conflicts with a function.
Keys that exist in only one map are copied into the result. If a key exists in
both maps and both values are maps, those maps are recursively merged. If a key
exists in both maps and either value is not a map, fun is called with the
left and right values and its return value is used.
This is analogous to Ramda's mergeDeepWith.
Examples
iex> Algo.Assoc.deep_merge_with(%{a: true, c: %{values: [10, 20]}}, %{b: true, c: %{values: [15, 35]}}, fn left, right -> left ++ right end)
%{a: true, b: true, c: %{values: [10, 20, 15, 35]}}
iex> Algo.Assoc.deep_merge_with(%{a: 1, c: %{d: 2}}, %{a: 10, b: 20, c: %{e: 3}}, fn _left, right -> right end)
%{a: 10, b: 20, c: %{d: 2, e: 3}}
Deletes the entry at a nested path in a map or keyword list.
Missing paths, empty paths, and paths that try to traverse through a non-nested value return the input unchanged.
For keyword lists, traversal follows first-match lookup. Ancestor path
segments are rewritten with Keyword.put/3 after deletion, so duplicate keys
along the path are removed. When the deleted key is at the current keyword-list
level, all duplicate entries for that key are removed, matching
Keyword.delete/2.
Examples
iex> Algo.Assoc.delete_path(%{a: %{b: 1, c: 2}}, [:a, :b])
%{a: %{c: 2}}
iex> Algo.Assoc.delete_path(%{a: 1}, [:a, :b])
%{a: 1}
iex> Algo.Assoc.delete_path([a: [b: 1], a: [b: 2]], [:a, :b])
[a: []]
Calls a function for each entry in a map or keyword list using breadth-first traversal.
Values that are maps or keyword lists are traversed recursively after all entries at the current level have been yielded. Other lists are treated as leaf values. Keyword list order and duplicate keys are preserved.
Examples
iex> Process.put(:visited, [])
iex> fun = fn entry -> Process.put(:visited, [entry | Process.get(:visited, [])]) end
iex> Algo.Assoc.each_breadth_first([a: [b: 1], c: %{d: 2}, e: [1, 2]], fun)
:ok
iex> Process.get(:visited) |> Enum.reverse()
[a: [b: 1], c: %{d: 2}, e: [1, 2], b: 1, d: 2]
Calls a function for each entry in a map or keyword list using depth-first traversal.
Values that are maps or keyword lists are traversed recursively after their containing entry is yielded. Other lists are treated as leaf values. Keyword list order and duplicate keys are preserved.
Examples
iex> Process.put(:visited, [])
iex> fun = fn entry -> Process.put(:visited, [entry | Process.get(:visited, [])]) end
iex> Algo.Assoc.each_depth_first([a: [b: 1], c: %{d: 2}, e: [1, 2]], fun)
:ok
iex> Process.get(:visited) |> Enum.reverse()
[a: [b: 1], b: 1, c: %{d: 2}, d: 2, e: [1, 2]]
Returns whether a map and keyword list contain equivalent entries.
The map is converted to a keyword list before comparison, so nested maps are compared as nested keyword lists. Keyword-list duplicate keys are significant.
Examples
iex> Algo.Assoc.equivalent?(%{a: 1, b: 2}, b: 2, a: 1)
true
iex> Algo.Assoc.equivalent?(%{a: 1}, a: 1, a: 2)
false
Updates values in a map or keyword list using a similarly-shaped evolution map.
Leaf values in the evolution map are functions that receive the current value at the same path in the input and return the replacement value. Intermediate values may be maps or keyword lists, and they may be mixed.
For keyword lists, updates follow Elixir's update_in/3 keyword-list behavior:
the first matching key is updated and duplicate keys are preserved.
Examples
iex> Algo.Assoc.evolve(%{a: %{b: 1}, c: 2}, %{a: %{b: &(&1 + 1)}})
%{a: %{b: 2}, c: 2}
iex> Algo.Assoc.evolve([a: [b: 1], c: 2], a: [b: &(&1 + 1)])
[a: [b: 2], c: 2]
iex> Algo.Assoc.evolve([a: 1, a: 2], a: &(&1 + 1))
[a: 2, a: 2]
Fetches the value at a nested path in a map or keyword list.
Returns {:ok, value} when the full path exists, otherwise :error. Maps and
keyword lists may be mixed at different levels. Empty paths are treated as
absent.
For keyword lists, lookup follows Keyword.fetch/2: duplicate keys resolve to
the first matching entry.
Examples
iex> Algo.Assoc.fetch_path(%{a: %{b: nil}}, [:a, :b])
{:ok, nil}
iex> Algo.Assoc.fetch_path([a: [b: 1]], [:a, :b])
{:ok, 1}
iex> Algo.Assoc.fetch_path(%{a: [b: 1]}, [:a, :c])
:error
Fetches the value at a nested path in a map or keyword list, raising if absent.
Raises KeyError when the full path does not exist.
Examples
iex> Algo.Assoc.fetch_path!(%{a: %{b: 1}}, [:a, :b])
1
iex> Algo.Assoc.fetch_path!([a: [b: 1]], [:a, :b])
1
Counts the maximum number of nested map or keyword-list levels below the root.
A map or keyword list with no nested maps or keyword lists has depth 0.
Mixed map/keyword-list nesting is counted. Ordinary non-keyword lists are
treated as leaf values.
Examples
iex> Algo.Assoc.get_depth(%{a: 1, b: 2})
0
iex> Algo.Assoc.get_depth(%{a: %{b: 1}})
1
iex> Algo.Assoc.get_depth(a: [b: [c: 1]])
2
Gets the value at a nested path in a map or keyword list.
Missing paths return default. Maps and keyword lists may be mixed at
different levels. Empty paths are treated as absent.
For keyword lists, lookup follows Keyword.get/3: duplicate keys resolve to
the first matching entry.
Examples
iex> Algo.Assoc.get_path(%{a: %{b: nil}}, [:a, :b], :missing)
nil
iex> Algo.Assoc.get_path([a: [b: 1]], [:a, :b])
1
iex> Algo.Assoc.get_path(%{a: [b: 1]}, [:a, :c], :missing)
:missing
Returns each leaf value in a map or keyword list with the path needed to reach it.
Each result is a {path, value} tuple. The path is a list of keys from the
root to the leaf. Maps and keyword lists are traversed as nested structures;
other lists are treated as leaf values.
Map iteration order is not guaranteed, so callers should not rely on the order of results from maps. Keyword-list order is preserved.
Examples
iex> Algo.Assoc.get_paths(%{a: %{b: 1}})
[{[:a, :b], 1}]
iex> Algo.Assoc.get_paths(%{a: %{b: 1}, c: %{d: 2}}) |> Enum.sort()
[{[:a, :b], 1}, {[:c, :d], 2}]
iex> Algo.Assoc.get_paths(a: %{b: 1}, c: [d: [e: 2]])
[{[:a, :b], 1}, {[:c, :d, :e], 2}]
iex> Algo.Assoc.get_paths(a: 1, a: 2, b: [1, 2, 3])
[{[:a], 1}, {[:a], 2}, {[:b], [1, 2, 3]}]
Returns the values at several nested paths.
Missing paths return nil. For keyword lists, lookup follows
Keyword.get/2: duplicate keys resolve to the first matching entry.
Examples
iex> Algo.Assoc.get_values_at_paths(%{a: %{b: 1}, c: 2}, [[:a, :b], [:c], [:missing]])
[1, 2, nil]
iex> Algo.Assoc.get_values_at_paths([a: [b: 1], a: [b: 2]], [[:a, :b]])
[1]
Returns whether a map or keyword list contains the given path.
Paths are lists of keys to traverse from the root to the target key. Maps and keyword lists may be mixed at different levels. Empty paths are treated as absent.
For keyword lists, lookup follows Keyword.fetch/2: duplicate keys resolve to
the first matching entry.
Examples
iex> Algo.Assoc.has_path?(%{a: %{b: nil}}, [:a, :b])
true
iex> Algo.Assoc.has_path?([a: [b: 1]], [:a, :b])
true
iex> Algo.Assoc.has_path?(%{a: [b: 1]}, [:a, :c])
false
Inverts a map by grouping original keys under their stringified values.
Each value in the input map becomes a string key in the returned map, and the
returned value is a list of input keys that had that value. Values are
stringified with to_string/1, so values such as 1 and "1" are grouped
together.
The order of keys within each grouped list follows map enumeration order, so callers should not rely on it.
Examples
iex> Algo.Assoc.invert(%{a: 1, b: 2})
%{"1" => [:a], "2" => [:b]}
iex> Algo.Assoc.invert(%{a: 1, b: "1"}) |> Map.update!("1", &Enum.sort/1)
%{"1" => [:a, :b]}
iex> Algo.Assoc.invert(%{})
%{}
Merges two maps or two keyword lists, preferring values from the left input.
For maps, this is equivalent to Map.merge(right_map, left_map).
For keyword lists, this is equivalent to Keyword.merge(right_kw_list, left_kw_list):
entries from the right input are kept unless their key appears in the left
input, and entries from the left input are appended. Duplicate keys from the
left input are preserved.
Examples
iex> Algo.Assoc.merge_left(%{a: 1, b: 2}, %{a: 10, c: 3})
%{a: 1, b: 2, c: 3}
iex> Algo.Assoc.merge_left([a: 1, b: 2], [a: 10, c: 3])
[c: 3, a: 1, b: 2]
iex> Algo.Assoc.merge_left([a: 1, a: 2, b: 3], [a: 10, a: 11])
[a: 1, a: 2, b: 3]
Merges two maps or two keyword lists, preferring values from the right input.
For maps, this is equivalent to Map.merge/2.
For keyword lists, this is equivalent to Keyword.merge/2: entries from the
left input are kept unless their key appears in the right input, and entries
from the right input are appended. Duplicate keys from the right input are
preserved.
Examples
iex> Algo.Assoc.merge_right(%{a: 1, b: 2}, %{a: 10, c: 3})
%{a: 10, b: 2, c: 3}
iex> Algo.Assoc.merge_right([a: 1, b: 2], [a: 10, c: 3])
[b: 2, a: 10, c: 3]
iex> Algo.Assoc.merge_right([a: 1, a: 2, b: 3], [a: 10, a: 11])
[b: 3, a: 10, a: 11]
Returns whether a value at a nested path satisfies a predicate.
The predicate is called only when the full path exists. Missing paths return
false. For keyword lists, lookup follows first-match behavior.
Examples
iex> Algo.Assoc.path_satisfies?(%{a: %{b: 3}}, [:a, :b], &(&1 > 2))
true
iex> Algo.Assoc.path_satisfies?(%{a: nil}, [:missing], &is_nil/1)
false
Keeps only the requested keys from each map or keyword list in a list. An analogue of the SQL
select statement.
Missing keys are ignored. For keyword lists, duplicate keys are preserved when
they are requested, matching Keyword.take/2 behavior.
Examples
iex> Algo.Assoc.project([%{a: 1, b: 2, c: 3}, %{a: 4, c: 6}], [:a, :b])
[%{a: 1, b: 2}, %{a: 4}]
iex> Algo.Assoc.project([[a: 1, b: 2, c: 3, a: 5]], [:a, :c])
[[a: 1, c: 3, a: 5]]
Returns a function that gets a key from a map or keyword list.
Missing keys return nil. For keyword lists, lookup follows
Keyword.get/2: duplicate keys resolve to the first matching entry.
Examples
iex> Algo.Assoc.prop(:name).(%{name: "Ada", age: 36})
"Ada"
iex> Algo.Assoc.prop(:name).([name: "Ada", name: "Grace"])
"Ada"
iex> Algo.Assoc.prop(:missing).(%{})
nil
Returns a function that keeps only the requested keys from a map or keyword list.
Missing keys are ignored. For keyword lists, duplicate keys are preserved when
they are requested, matching Keyword.take/2 behavior.
Examples
iex> Algo.Assoc.props([:name, :email]).(%{name: "Ada", email: "ada@example.com", age: 36})
%{email: "ada@example.com", name: "Ada"}
iex> Algo.Assoc.props([:name, :email]).([name: "Ada", age: 36, name: "Grace"])
[name: "Ada", name: "Grace"]
Puts a value at a nested path only when the full path is absent.
This is the path equivalent of Map.put_new/3 and Keyword.put_new/3.
Missing intermediate containers are created using the same rules as
put_path/3.
Examples
iex> Algo.Assoc.put_new_path(%{a: %{b: 1}}, [:a, :b], 2)
%{a: %{b: 1}}
iex> Algo.Assoc.put_new_path(%{}, [:a, :b], 1)
%{a: %{b: 1}}
Puts a value at a nested path in a map or keyword list.
Missing intermediate containers are created. Missing children under maps become maps, and missing children under keyword lists become keyword lists. Existing map or keyword-list children keep their type. Existing non-container children are replaced by new nested containers.
Empty paths are treated as absent and return the input unchanged.
For keyword lists, each path segment follows Keyword.put/3: duplicate
entries for keys along the path are removed. When a duplicated ancestor key
already exists, the first matching value is used as the child to update before
the duplicate entries are collapsed.
Examples
iex> Algo.Assoc.put_path(%{a: %{b: 1}}, [:a, :b], 2)
%{a: %{b: 2}}
iex> Algo.Assoc.put_path(%{c: 3}, [:a, :b], 1)
%{a: %{b: 1}, c: 3}
iex> Algo.Assoc.put_path([a: [b: 1], a: [b: 2]], [:a, :b], 3)
[a: [b: 3]]
Renames keys in a map or keyword list.
This is a shallow operation. Keys not present in the rename map are preserved, and missing source keys are ignored.
For maps, renamed keys are overlaid after unchanged keys, so renamed values win when they collide with an existing unchanged key. If multiple source keys are renamed to the same target key, the last rename entry wins; map rename order is subject to normal map enumeration order, so use a keyword list when ordering matters.
For keyword lists, every matching entry is renamed in place. Order and duplicates are preserved, and collisions are not collapsed.
Examples
iex> Algo.Assoc.rename_keys(%{first_name: "Ada", age: 36}, %{first_name: :name})
%{name: "Ada", age: 36}
iex> Algo.Assoc.rename_keys([a: 1, a: 2, b: 3], %{a: :x})
[x: 1, x: 2, b: 3]
Replaces the value at a nested path if the full path exists.
Missing paths return the input unchanged. For keyword lists, the target key
follows Keyword.replace/3: duplicate entries for that key at the target level
are removed.
Examples
iex> Algo.Assoc.replace_path(%{a: %{b: 1}}, [:a, :b], 2)
%{a: %{b: 2}}
iex> Algo.Assoc.replace_path(%{a: 1}, [:a, :b], 2)
%{a: 1}
Replaces the value at a nested path, raising if the full path is absent.
Raises KeyError when the full path does not exist.
Examples
iex> Algo.Assoc.replace_path!(%{a: %{b: 1}}, [:a, :b], 2)
%{a: %{b: 2}}
Recursively converts map keys to strings.
Nested maps are converted recursively. Other values, including keyword lists, are left unchanged.
Examples
iex> Algo.Assoc.stringify_keys(%{a: 1, b: %{c: 2}})
%{"a" => 1, "b" => %{"c" => 2}}
iex> Algo.Assoc.stringify_keys(%{})
%{}
Flattens nested map or keyword list keys into a single level.
Nested maps and keyword lists are recursively removed while their leaf entries are kept. Non-keyword lists are treated as leaf values.
When a key appears both at the current level and inside a nested structure, the current-level value wins. Keyword list output preserves ordering and duplicate leaf entries that are not shadowed by a higher-level key.
Examples
iex> Algo.Assoc.unnest_keys(%{a: %{b: 1}, c: %{d: 2}, e: 3})
%{b: 1, d: 2, e: 3}
iex> Algo.Assoc.unnest_keys(a: [b: 1], c: [d: 2], e: 3)
[b: 1, d: 2, e: 3]
iex> Algo.Assoc.unnest_keys(c: "original", a: [b: [c: "nested"]])
[c: "original"]
Updates the value at a nested path in a map or keyword list.
If the full path exists, fun is called with the current value and its result
is stored. If the full path is absent, initial is stored without calling
fun. Missing intermediate containers are created using the same rules as
put_path/3.
This is the path equivalent of Map.update/4 and Keyword.update/4. For
keyword lists, duplicate entries for the target key at the target level are
removed.
Examples
iex> Algo.Assoc.update_path(%{a: %{b: 1}}, [:a, :b], 0, &(&1 + 1))
%{a: %{b: 2}}
iex> Algo.Assoc.update_path(%{a: 1}, [:a, :b], 0, &(&1 + 1))
%{a: %{b: 0}}
iex> Algo.Assoc.update_path([a: 1, a: 2], [:a], 0, &(&1 + 10))
[a: 11]
Updates the value at a nested path, raising if the full path is absent.
Raises KeyError when the full path does not exist.
Examples
iex> Algo.Assoc.update_path!(%{a: %{b: 1}}, [:a, :b], &(&1 + 1))
%{a: %{b: 2}}
Returns whether a map or keyword list satisfies every keyed predicate.
The condition map or keyword list contains functions keyed by the values they
should check. Missing keys are passed to the predicate as nil.
For keyword lists, lookup follows Keyword.get/2: duplicate keys resolve to
the first matching entry.
Examples
iex> Algo.Assoc.where_all?(%{a: 1, b: 2}, %{a: &(&1 == 1), b: &(&1 > 1)})
true
iex> Algo.Assoc.where_all?([a: 1, b: 2], a: &(&1 == 1), b: &(&1 > 1))
true
iex> Algo.Assoc.where_all?([a: 1, a: 2], a: &(&1 == 2))
false
Returns whether any keyed predicate is satisfied by a map or keyword list.
Conditions may be nested maps or keyword lists. Missing paths are passed to the
predicate as nil, matching where_all?/2 missing-key behavior. Empty condition
containers return false.
For keyword lists, lookup follows first-match behavior.
Examples
iex> Algo.Assoc.where_any?(%{a: 1, b: 2}, %{a: &(&1 == 0), b: &(&1 == 2)})
true
iex> Algo.Assoc.where_any?([a: 1, a: 2], a: &(&1 == 2))
false
Returns whether a map or keyword list contains equal values at all condition paths.
The condition container may be nested. Missing paths compare as nil, matching
where_all?/2 missing-key behavior. Empty condition containers return true.
For keyword lists, lookup follows first-match behavior.
Examples
iex> Algo.Assoc.where_eq?(%{a: %{b: 1}, c: 2}, %{a: %{b: 1}})
true
iex> Algo.Assoc.where_eq?([a: 1, a: 2], a: 2)
false