Algo.Assoc (Algo v0.1.0)

Copy Markdown View Source

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

as_keyword_list(map)

@spec as_keyword_list(map() | keyword()) :: keyword()

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(%{})
[]

as_map(map)

@spec as_map(map() | keyword()) :: map()

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]}

atomise_keys_with(map, key_fun)

@spec atomise_keys_with(map(), (String.t() -> atom())) :: map()

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)
%{}

atomize_keys_with(map, key_fun)

See Algo.Assoc.atomise_keys_with/2.

deep_merge_left(left_map, right_map)

@spec deep_merge_left(map(), map()) :: map()

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}

deep_merge_right(left_map, right_map)

@spec deep_merge_right(map(), map()) :: map()

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}

deep_merge_with(left_map, right_map, fun)

@spec deep_merge_with(map(), map(), (any(), any() -> any())) :: map()

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}}

delete_path(assoc, arg2)

@spec delete_path(map() | keyword(), list()) :: map() | keyword()

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: []]

each_breadth_first(map, fun)

@spec each_breadth_first(map() | keyword(), ({any(), any()} -> any())) :: :ok

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]

each_depth_first(map, fun)

@spec each_depth_first(map() | keyword(), ({any(), any()} -> any())) :: :ok

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]]

equivalent?(map, kw_list)

@spec equivalent?(
  map(),
  keyword()
) :: boolean()

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

evolve(map, evolution_map)

@spec evolve(map() | keyword(), map() | keyword()) :: map() | keyword()

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]

fetch_path(assoc, arg2)

@spec fetch_path(map() | keyword(), list()) :: {:ok, any()} | :error

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

fetch_path!(assoc, path)

@spec fetch_path!(map() | keyword(), list()) :: any()

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

get_depth(assoc)

@spec get_depth(any()) :: integer()

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

get_path(assoc, path, default \\ nil)

@spec get_path(map() | keyword(), list(), any()) :: any()

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

get_paths(map)

@spec get_paths(map() | keyword()) :: [{list(), any()}]

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]}]

get_values_at_paths(map, paths)

@spec get_values_at_paths(map() | keyword(), [list()]) :: list()

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]

has_path?(assoc, path)

@spec has_path?(map() | keyword(), list()) :: boolean()

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

invert(map)

@spec invert(map()) :: map()

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(%{})
%{}

merge_left(left_map, right_map)

@spec merge_left(map(), map()) :: map()
@spec merge_left(keyword(), keyword()) :: keyword()

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]

merge_right(left_map, right_map)

@spec merge_right(map(), map()) :: map()
@spec merge_right(keyword(), keyword()) :: keyword()

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]

path_satisfies?(map, path, fun)

@spec path_satisfies?(map() | keyword(), list(), (any() -> as_boolean(term()))) ::
  boolean()

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

project(list_of_maps, keys)

@spec project([map() | keyword()], list()) :: [map() | keyword() | :error]

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]]

prop(name)

@spec prop(any()) :: (map() | keyword() -> any() | :error)

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

props(names)

@spec props(list()) :: (map() | keyword() -> map() | keyword() | :error)

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"]

put_new_path(assoc, path, value)

@spec put_new_path(map() | keyword(), list(), any()) :: map() | keyword()

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}}

put_path(assoc, arg2, value)

@spec put_path(map() | keyword(), list(), any()) :: map() | keyword()

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]]

rename_keys(map, renames)

@spec rename_keys(map() | keyword(), map() | keyword()) :: map() | keyword()

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]

replace_path(assoc, path, value)

@spec replace_path(map() | keyword(), list(), any()) :: map() | keyword()

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}

replace_path!(assoc, path, value)

@spec replace_path!(map() | keyword(), list(), any()) :: map() | keyword()

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}}

stringify_keys(map)

@spec stringify_keys(map()) :: map()

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(%{})
%{}

unnest_keys(map)

@spec unnest_keys(map() | keyword()) :: map() | keyword()

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"]

update_path(assoc, path, initial, fun)

@spec update_path(map() | keyword(), list(), any(), (any() -> any())) ::
  map() | keyword()

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]

update_path!(assoc, path, fun)

@spec update_path!(map() | keyword(), list(), (any() -> any())) :: map() | keyword()

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}}

where_all?(map, condition_map)

@spec where_all?(map() | keyword(), map() | keyword()) :: boolean() | :error

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

where_any?(map, condition_map)

@spec where_any?(map() | keyword(), map() | keyword()) :: boolean()

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

where_eq?(map, condition_map)

@spec where_eq?(map() | keyword(), map() | keyword()) :: boolean()

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