defmodule Cldr.Map do @moduledoc """ Functions for transforming maps, keys and values. """ @doc """ Returns am argument unchanged. Useful when a `noop` function is required. """ def identity(x), do: x @default_deep_map_options [level: 1..1_000_000, filter: [], reject: [], skip: [], only: [], except: []] @starting_level 1 @max_level 1_000_000 @doc """ Recursively traverse a map and invoke a function that transforms the mapfor each key/value pair. ## Arguments * `map` is any `t:map/0` * `function` is a `1-arity` function or function reference that is called for each key/value pair of the provided map. It can also be a 2-tuple of the form `{key_function, value_function}` * In the case where `function` is a single function it will be called with the 2-tuple argument `{key, value}` * In the case where function is of the form `{key_function, value_function}` the `key_function` will be called with the argument `key` and the value function will be called with the argument `value` * `options` is a keyword list of options. The default is `#{inspect @default_deep_map_options}` ## Options * `:level` indicates the starting (and optionally ending) levels of the map at which the `function` is executed. This can be an integer representing one `level` or a range indicating a range of levels. The default is `1..#{@max_level}` * `:only` is a term or list of terms or a `check function`. If it is a term or list of terms, the `function` is only called if the `key` of the map is equal to the term or in the list of terms. If `:only` is a `check function` then the `check function` is passed the `{k, v}` of the current branch in the `map`. It is expected to return a `truthy` value that if `true` signals that the argument `function` will be executed. * `:except` is a term or list of terms or a `check function`. If it is a term or list of terms, the `function` is only called if the `key` of the map is not equal to the term or not in the list of terms. If `:except` is a `check function` then the `check function` is passed the `{k, v}` of the current branch in the `map`. It is expected to return a `truthy` value that if `true` signals that the argument `function` will not be executed. * `:filter` is a term or list of terms or a `check function`. If the `key` currently being processed equals the term (or is in the list of terms, or the `check_function` returns a truthy value) then this branch of the map is processed by `function` and its output is included in the result. * `:reject` is a term or list of terms or a `check function`. If the `key` currently being processed equals the term (or is in the list of terms, or the `check_function` returns a truthy value) then this branch of the map is omitted from the mapped output. * `:skip` is a term or list of terms or a `check function`. If the `key` currently being processed equals the term (or is in the list of terms, or the `check_function` returns a truthy value) then this branch of the map is *not* processed by `function` but it is included in the mapped result. ## Notes * `:only` and `:except` operate on individual keys whereas `:filter` and `:filter` and `:reject` operator on *entire branches* of a map * If both the options `:only` and `:except` are provided then the `function` is called only when a `term` meets both criteria. That means that `:except` has priority over `:only`. * If both the options `:filter` and `:reject` are provided then `:reject` has priority over `:filter`. ## Returns * The `map` transformed by the recursive application of `function` ## Examples iex> map = %{a: :a, b: %{c: :c}} iex> fun = fn ...> {k, v} when is_atom(k) -> {Atom.to_string(k), v} ...> other -> other ...> end iex> Cldr.Map.deep_map map, fun %{"a" => :a, "b" => %{"c" => :c}} iex> map = %{a: :a, b: %{c: :c}} iex> Cldr.Map.deep_map map, fun, only: :c %{a: :a, b: %{"c" => :c}} iex> Cldr.Map.deep_map map, fun, except: [:a, :b] %{a: :a, b: %{"c" => :c}} iex> Cldr.Map.deep_map map, fun, level: 2 %{a: :a, b: %{"c" => :c}} """ @spec deep_map( map() | list(), function :: function() | {function(), function()}, options :: list() ) :: map() | list() def deep_map(map, function, options \\ @default_deep_map_options) # Don't deep map structs since they have atom keys anyway and they # also don't support enumerable def deep_map(%_struct{} = map, _function, _options) when is_map(map) do map end def deep_map(map_or_list, function, options) when (is_map(map_or_list) or is_list(map_or_list)) and is_list(options) do options = validate_options(function, options) deep_map(map_or_list, function, options, @starting_level) end defp deep_map(nil, _fun, _options, _level) do nil end # If the level is greater than the return # just return the map or list defp deep_map(map_or_list, _function, %{level: %{last: last}}, level) when level > last do map_or_list end # If the level is less than the range then keep recursing # without executing the function defp deep_map(map, function, %{level: %{first: first}} = options, level) when is_map(map) and level < first do Enum.map(map, fn {k, v} when is_map(v) or is_list(v) -> {k, deep_map(v, function, options, level + 1)} {k, v} -> {k, v} end) |> Map.new() end # Here we are in range so we conditionally execute the function # if the options for `:only` and `:except` are matched defp deep_map(map, function, options, level) when is_map(map) and is_function(function) do Enum.reduce(map, [], fn {k, v}, acc when is_map(v) or is_list(v) -> case process_type({k, v}, options) do :process -> v = deep_map(v, function, options, level + 1) [function.({k, v}) | acc] :except -> v = deep_map(v, function, options, level + 1) [{k, v} | acc] :skip -> [function.({k, v}) | acc] :reject -> acc end {k, v}, acc -> case process_type({k, v}, options) do :process -> [function.({k, v}) | acc] :except -> [{k, v} | acc] :skip -> [function.({k, v}) | acc] :reject -> acc end end) |> Map.new() end defp deep_map(map, {key_function, value_function}, options, level) when is_map(map) do Enum.reduce(map, [], fn {k, v}, acc when is_map(v) or is_list(v) -> case process_type({k, v}, options) do :process -> v = deep_map(v, {key_function, value_function}, options, level + 1) [{key_function.(k), value_function.(v)} | acc] :except -> v = deep_map(v, {key_function, value_function}, options, level + 1) [{k, v} | acc] :skip -> [{key_function.(k), v} | acc] :reject -> acc end {k, v}, acc -> case process_type({k, v}, options) do :process -> [{key_function.(k), value_function.(v)} | acc] :except -> [{k, v} | acc] :skip -> [{key_function.(k), v} | acc] :reject -> acc end end) |> Map.new() end defp deep_map([], _function, _options, _level) do [] end defp deep_map([head | rest], function, options, level) do case process_type(head, options) do :process -> [deep_map(head, function, options, level + 1) | deep_map(rest, function, options, level + 1)] :except -> [deep_map(head, function, options, level + 1) | deep_map(rest, function, options, level + 1)] :skip -> [head | deep_map(rest, function, options, level + 1)] :reject -> deep_map(rest, function, options, level + 1) end end defp deep_map(value, function, options, _level) when is_function(function) do case process_type(value, options) do :process -> function.(value) :except -> value :skip -> value :reject -> nil end end defp deep_map(value, {_key_function, value_function}, options, _level) do case process_type(value, options) do :process -> value_function.(value) :skip -> value :reject -> nil end end @doc """ Transforms a `map`'s `String.t` keys to `atom()` keys. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3`. One additional option apples to this function directly: * `:only_existing` which is set to `true` will only convert the binary value to an atom if the atom already exists. The default is `false`. ## Example iex> Cldr.Map.atomize_keys %{"a" => %{"b" => %{1 => "c"}}} %{a: %{b: %{1 => "c"}}} """ @default_atomize_options [only_existing: false] def atomize_keys(map, options \\ []) def atomize_keys(map, options) when is_map(map) or is_list(map) do options = @default_atomize_options ++ options deep_map(map, &atomize_key(&1, Map.new(options)), options) end def atomize_keys({k, value}, options) when is_map(value) or is_list(value) do options = @default_atomize_options ++ options map_options = Map.new(options) {atomize_key(k, map_options), deep_map(value, &atomize_key(&1, map_options), options)} end # For compatibility with older # versions of ex_cldr that expect this # behaviour # TODO Fix Cldr.Config.get_locale to not make this assumption def atomize_keys(other, _options) do other end @doc """ Transforms a `map`'s `String.t` values to `atom()` values. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3`. One additional option apples to this function directly: * `:only_existing` which is set to `true` will only convert the binary value to an atom if the atom already exists. The default is `false`. ## Examples iex> Cldr.Map.atomize_values %{"a" => %{"b" => %{1 => "c"}}} %{"a" => %{"b" => %{1 => :c}}} """ def atomize_values(map, options \\ [only_existing: false]) def atomize_values(map, options) when is_map(map) or is_list(map) do options = @default_atomize_options ++ options deep_map(map, &atomize_value(&1, Map.new(options)), options) end def atomize_values({k, value}, options) when is_map(value) or is_list(value) do options = @default_atomize_options ++ options map_options = Map.new(options) {k, deep_map(value, &atomize_value(&1, map_options), options)} end @doc """ Transforms a `map`'s `String.t` keys to `Integer.t` keys. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` The map key is converted to an `integer` from either an `atom` or `String.t` only when the key is comprised of `integer` digits. Keys which cannot be converted to an `integer` are returned unchanged. ## Example iex> Cldr.Map.integerize_keys %{a: %{"1" => "value"}} %{a: %{1 => "value"}} """ def integerize_keys(map, options \\ []) def integerize_keys(map, options) when is_map(map) or is_list(map) do deep_map(map, &integerize_key/1, options) end def integerize_keys({k, value}, options) when is_map(value) or is_list(value) do {integerize_key(k), deep_map(value, &integerize_key/1, options)} end @doc """ Transforms a `map`'s `String.t` values to `Integer.t` values. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` The map value is converted to an `integer` from either an `atom` or `String.t` only when the value is comprised of `integer` digits. Keys which cannot be converted to an integer are returned unchanged. ## Example iex> Cldr.Map.integerize_values %{a: %{b: "1"}} %{a: %{b: 1}} """ def integerize_values(map, options \\ []) do deep_map(map, &integerize_value/1, options) end @doc """ Transforms a `map`'s `String.t` keys to `Float.t` values. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` The map key is converted to a `float` from a `String.t` only when the key is comprised of a valid float form. Keys which cannot be converted to a `float` are returned unchanged. ## Examples iex> Cldr.Map.floatize_keys %{a: %{"1.0" => "value"}} %{a: %{1.0 => "value"}} iex> Cldr.Map.floatize_keys %{a: %{"1" => "value"}} %{a: %{1.0 => "value"}} """ def floatize_keys(map, options \\ []) def floatize_keys(map, options) when is_map(map) or is_list(map) do deep_map(map, &floatize_key/1, options) end def floatize_keys({k, value}, options) when is_map(value) or is_list(value) do {floatize_key(k), deep_map(value, &floatize_key/1, options)} end @doc """ Transforms a `map`'s `String.t` values to `Float.t` values. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` The map value is converted to a `float` from a `String.t` only when the value is comprised of a valid float form. Values which cannot be converted to a `float` are returned unchanged. ## Examples iex> Cldr.Map.floatize_values %{a: %{b: "1.0"}} %{a: %{b: 1.0}} iex> Cldr.Map.floatize_values %{a: %{b: "1"}} %{a: %{b: 1.0}} """ def floatize_values(map, options \\ []) do deep_map(map, &floatize_value/1, options) end @doc """ Transforms a `map`'s `atom()` keys to `String.t` keys. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` ## Example iex> Cldr.Map.stringify_keys %{a: %{"1" => "value"}} %{"a" => %{"1" => "value"}} """ def stringify_keys(map, options \\ []) def stringify_keys(map, options) when is_map(map) or is_list(map) do deep_map(map, &stringify_key/1, options) end def stringify_keys({k, value}, options) when is_map(value) or is_list(value) do {stringify_key(k), deep_map(value, &stringify_key/1, options)} end @doc """ Transforms a `map`'s `atom()` keys to `String.t` keys. ## Arguments * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` ## Example iex> Cldr.Map.stringify_values %{a: %{"1" => :value}} %{a: %{"1" => "value"}} """ def stringify_values(map, options \\ []) do deep_map(map, &stringify_value/1, options) end @doc """ Convert map `String.t` keys from `camelCase` to `snake_case` * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` ## Example iex> Cldr.Map.underscore_keys %{"a" => %{"thisOne" => "value"}} %{"a" => %{"this_one" => "value"}} """ def underscore_keys(map, options \\ []) def underscore_keys(map, options) when is_map(map) or is_nil(map) do deep_map(map, &underscore_key/1, options) end def underscore_keys({k, value}, options) when is_map(value) or is_list(value) do {underscore_key(k), deep_map(value, &underscore_key/1, options)} end @doc """ Rename map keys from `from` to `to` * `map` is any `t:map/0` * `from` is any value map key * `to` is any valid map key * `options` is a keyword list of options passed to `deep_map/3` ## Example iex> Cldr.Map.rename_keys %{"a" => %{"this_one" => "value"}}, "this_one", "that_one" %{"a" => %{"that_one" => "value"}} """ def rename_keys(map, from, to, options \\ []) when is_map(map) or is_list(map) do renamer = fn {^from, v} -> {to, v} other -> other end deep_map(map, renamer, options) end @doc """ Convert a camelCase string or atom to a snake_case * `string` is a `String.t` or `atom()` to be transformed This is the code of Macro.underscore with modifications. The change is to cater for strings in the format: This_That which in Macro.underscore gets formatted as this__that (note the double underscore) when we actually want that_that ## Examples iex> Cldr.Map.underscore "thisThat" "this_that" iex> Cldr.Map.underscore "This_That" "this_that" """ @spec underscore(string :: String.t() | atom()) :: String.t() def underscore(<>) do <> <> do_underscore(t, h) end def underscore(other) do other end # h is upper case, next char is not uppercase, or a _ or . => and prev != _ defp do_underscore(<>, prev) when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and t != ?. and t != ?_ and t != ?- and prev != ?_ do <> <> do_underscore(rest, t) end # h is uppercase, previous was not uppercase or _ defp do_underscore(<>, prev) when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ do <> <> do_underscore(t, h) end # h is dash "-" -> replace with underscore "_" defp do_underscore(<>, _) do <> <> underscore(t) end # h is . defp do_underscore(<>, _) do <> <> underscore(t) end # Any other char defp do_underscore(<>, _) do <> <> do_underscore(t, h) end defp do_underscore(<<>>, _) do <<>> end defp to_lower_char(char) when char == ?-, do: ?_ defp to_lower_char(char) when char >= ?A and char <= ?Z, do: char + 32 defp to_lower_char(char), do: char @doc """ Removes any leading underscores from `map` `String.t` keys. * `map` is any `t:map/0` * `options` is a keyword list of options passed to `deep_map/3` ## Examples iex> Cldr.Map.remove_leading_underscores %{"a" => %{"_b" => "b"}} %{"a" => %{"b" => "b"}} """ def remove_leading_underscores(map, options \\ []) do remover = fn {k, v} when is_binary(k) -> {String.trim_leading(k, "_"), v} other -> other end deep_map(map, remover, options) end @doc """ Returns the result of deep merging a list of maps ## Examples iex> Cldr.Map.merge_map_list [%{a: "a", b: "b"}, %{c: "c", d: "d"}] %{a: "a", b: "b", c: "c", d: "d"} """ def merge_map_list(list, resolver \\ &standard_deep_resolver/3) def merge_map_list([h | []], _resolver) do h end def merge_map_list([h | t], resolver) do deep_merge(h, merge_map_list(t, resolver), resolver) end def merge_map_list([], _resolver) do [] end @doc """ Deep merge two maps * `left` is any `t:map/0` * `right` is any `t:map/0` ## Examples iex> Cldr.Map.deep_merge %{a: "a", b: "b"}, %{c: "c", d: "d"} %{a: "a", b: "b", c: "c", d: "d"} iex> Cldr.Map.deep_merge %{a: "a", b: "b"}, %{c: "c", d: "d", a: "aa"} %{a: "aa", b: "b", c: "c", d: "d"} """ def deep_merge(left, right, resolver \\ &standard_deep_resolver/3) when is_map(left) and is_map(right) do Map.merge(left, right, resolver) end # Key exists in both maps, and both values are maps as well. # These can be merged recursively. defp standard_deep_resolver(_key, left, right) when is_map(left) and is_map(right) do deep_merge(left, right, &standard_deep_resolver/3) end # Key exists in both maps, but at least one of the values is # NOT a map. We fall back to standard merge behavior, preferring # the value on the right. defp standard_deep_resolver(_key, _left, right) do right end def combine_list_resolver(_key, left, right) when is_list(left) and is_list(right) do left ++ right end @doc """ Delete all members of a map that have a key in the list of keys ## Examples iex> Cldr.Map.delete_in %{a: "a", b: "b"}, [:a] %{b: "b"} """ def delete_in(%{} = map, keys) when is_list(keys) do Enum.reject(map, fn {k, _v} -> k in keys end) |> Enum.map(fn {k, v} -> {k, delete_in(v, keys)} end) |> Map.new() end def delete_in(map, keys) when is_list(map) and is_binary(keys) do delete_in(map, [keys]) end def delete_in(map, keys) when is_list(map) do Enum.reject(map, fn {k, _v} -> k in keys end) |> Enum.map(fn {k, v} -> {k, delete_in(v, keys)} end) end def delete_in(%{} = map, keys) when is_binary(keys) do delete_in(map, [keys]) end def delete_in(other, _keys) do other end def from_keyword([] = list) do Map.new(list) end def from_keyword([{key, _value} | _rest] = keyword_list) when is_atom(key) do Map.new(keyword_list) end @doc """ Extract strings from a map or list Recursively process the map or list and extract string values from maps and string elements from lists """ def extract_strings(map_or_list, options \\ []) def extract_strings([], _options) do [] end def extract_strings(map, _options) when is_map(map) do Enum.reduce(map, [], fn {_k, v}, acc when is_binary(v) -> [v | acc] {_k, v}, acc when is_map(v) -> [extract_strings(v) | acc] {_k, v}, acc when is_list(v) -> [extract_strings(v) | acc] _other, acc -> acc end) |> List.flatten end def extract_strings(list, _options) when is_list(list) do Enum.reduce(list, [], fn v, acc when is_binary(v) -> [v | acc] v, acc when is_map(v) -> extract_strings(v, acc) v, acc when is_list(v) -> extract_strings(v, acc) _other, acc -> acc end) |> List.flatten end @doc """ Prune a potentially deeply nested map of some of its branches """ def prune(map, fun) when is_map(map) and is_function(fun, 1) do deep_map(map, &(&1), reject: fun) end @doc """ Invert a map Requires that the map is a simple map of keys and a list of values or a single non-map value """ def invert(map) when is_map(map) do map |> Enum.flat_map(fn {k, v} when is_list(v) -> Enum.map(v, fn vv -> {vv, k} end) {k, v} when not is_map(v) -> {v, k} end) |> Map.new() end # # Helpers # defp validate_options(function, options) when is_function(function) do validate_options(options) end defp validate_options({key_function, value_function}, options) when is_function(key_function) and is_function(value_function) do validate_options(options) end defp validate_options(function, _options) do raise ArgumentError, "function parameter must be a function or a 2-tuple " <> "consisting of a key_function and a value_function. Found #{inspect(function)}" end defp validate_options(options) do @default_deep_map_options |> Keyword.merge(options) |> Map.new() |> Map.update!(:level, fn level when is_integer(level) -> level..level %Range{} = level -> level other -> raise ArgumentError, ":level must be an integer or a range. Found #{inspect(other)}" end) end defp atomize_key({k, v}, %{only_existing: true}) when is_binary(k) do {String.to_existing_atom(k), v} rescue ArgumentError -> {k, v} end defp atomize_key({k, v}, %{only_existing: false}) when is_binary(k) do {String.to_atom(k), v} end defp atomize_key(other, _options) do other end defp atomize_value({k, v}, %{only_existing: true}) when is_binary(v) do {k, String.to_existing_atom(v)} rescue ArgumentError -> {k, v} end defp atomize_value({k, v}, %{only_existing: false}) when is_binary(v) do {k, String.to_atom(v)} end defp atomize_value(v, %{only_existing: false}) when is_binary(v) do String.to_atom(v) end defp atomize_value(other, _options) do other end defp integerize_key({k, v}) when is_binary(k) do case Integer.parse(k) do {integer, ""} -> {integer, v} _other -> {k, v} end end defp integerize_key(other) do other end defp integerize_value({k, v}) when is_binary(v) do case Integer.parse(v) do {integer, ""} -> {k, integer} _other -> {k, v} end end defp integerize_value(other) do other end defp floatize_key({k, v}) when is_binary(k) do case Float.parse(k) do {float, ""} -> {float, v} _other -> {k, v} end end defp floatize_key(other) do other end defp floatize_value({k, v}) when is_binary(v) do case Float.parse(v) do {float, ""} -> {k, float} _other -> {k, v} end end defp floatize_value(other) do other end defp stringify_key({k, v}) when is_atom(k), do: {Atom.to_string(k), v} defp stringify_key(other), do: other defp stringify_value({k, v}) when is_atom(v), do: {k, Atom.to_string(v)} defp stringify_value(other) when is_atom(other), do: Atom.to_string(other) defp stringify_value(other), do: other defp underscore_key({k, v}) when is_binary(k), do: {underscore(k), v} defp underscore_key(other), do: other # process_element?/2 determines whether the # calling function should apply to a given # value def process_type(x, options) do filter? = filter?(x, options) reject? = reject?(x, options) cond do reject? -> :reject skip?(x, options) -> :skip filter? && only?(x, options) && !except?(x, options) -> :process filter? -> :except end # |> IO.inspect(label: inspect(x)) end # Keep this branch but don't process it defp skip?(x, %{skip: skip}) when is_function(skip) do skip.(x) end defp skip?({k, _v}, %{skip: skip}) when is_list(skip) do k in skip end defp skip?({k, _v}, %{skip: skip}) do k == skip end defp skip?(k, %{skip: skip}) when is_list(skip) do k in skip end defp skip?(k, %{skip: skip}) do k == skip end # Keep this branch is the result defp filter?(x, %{filter: filter}) when is_function(filter) do filter.(x) end defp filter?(_x, %{filter: []}) do true end defp filter?({k, _v}, %{filter: filter}) when is_list(filter) do k in filter end defp filter?({k, _v}, %{filter: filter}) do k == filter end defp filter?(k, %{filter: filter}) when is_list(filter) do k in filter end defp filter?(k, %{filter: filter}) do k == filter end # Don't include this branch is the result defp reject?(x, %{reject: reject}) when is_function(reject) do reject.(x) end defp reject?({k, _v}, %{reject: reject}) when is_list(reject) do k in reject end defp reject?({k, _v}, %{reject: reject}) do k == reject end defp reject?(k, %{reject: reject}) when is_list(reject) do k in reject end defp reject?(k, %{reject: reject}) do k == reject end # Process this item defp only?(x, %{only: only}) when is_function(only) do only.(x) end defp only?(_x, %{only: []}) do true end defp only?({k, _v}, %{only: only}) when is_list(only) do k in only end defp only?({k, _v}, %{only: only}) do k == only end defp only?(k, %{only: only}) when is_list(only) do k in only end defp only?(k, %{only: only}) do k == only end # Don;t process this item defp except?(x, %{except: except}) when is_function(except) do except.(x) end defp except?({k, _v}, %{except: except}) when is_list(except) do k in except end defp except?({k, _v}, %{except: except}) do k == except end defp except?(k, %{except: except}) when is_list(except) do k in except end defp except?(k, %{except: except}) do k == except end end