# `Algo.Assoc`
[🔗](https://github.com/alexkorban/algo/blob/main/lib/algo/assoc.ex#L1)

Utility functions for for maps, keyword lists, and nested associative structures.

# `as_keyword_list`

```elixir
@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`

```elixir
@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`

```elixir
@spec atomise_keys_with(map(), (String.t() -&gt; 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`

# `deep_merge_left`

```elixir
@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`

```elixir
@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`

```elixir
@spec deep_merge_with(map(), map(), (any(), any() -&gt; 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`

```elixir
@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`

```elixir
@spec each_breadth_first(map() | keyword(), ({any(), any()} -&gt; 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`

```elixir
@spec each_depth_first(map() | keyword(), ({any(), any()} -&gt; 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?`

```elixir
@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`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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?`

```elixir
@spec path_satisfies?(map() | keyword(), list(), (any() -&gt; 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`

```elixir
@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`

```elixir
@spec prop(any()) :: (map() | keyword() -&gt; 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`

```elixir
@spec props(list()) :: (map() | keyword() -&gt; 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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec update_path(map() | keyword(), list(), any(), (any() -&gt; 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!`

```elixir
@spec update_path!(map() | keyword(), list(), (any() -&gt; 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?`

```elixir
@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?`

```elixir
@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?`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
