defmodule Morphix do @moduledoc """ Morphix provides convenience methods for dealing with Maps, Lists, and Tuples. `morphiflat/1` and `morphiflat!/1` flatten maps, discarding top level keys. ### Examples: ``` iex> Morphix.morphiflat %{flatten: %{this: "map"}, if: "you please"} {:ok, %{this: "map", if: "you please"}} iex> Morphix.morphiflat! %{flatten: %{this: "map"}, o: "k"} %{this: "map", o: "k"} ``` `morphify!/2` and `morphify/2` will take either a List or a Tuple as the first argument, and a function as the second. Returns a map, with the keys of the map being the function applied to each member of the input. ### Examples: ``` iex> Morphix.morphify!({[1,2,3], [12], [1,2,3,4]}, &length/1) %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]} ``` `atomorphify/1` and `atomorphiform/1` take a map as an input and return the map with all string keys converted to atoms. `atomorphiform/1` is recursive. `atomorphiform/2` and `atomormiphify/2` take `:safe` as a second argument, they will not convert string keys if the resulting atom has not been defined. ### Examples: ``` iex> Morphix.atomorphify(%{"a" => "2", :a => 2, 'a' => :two}) {:ok, %{:a => 2, 'a' => :two }} ``` `compactify` and `compactiform` take a map as an input and return a filtered map, removing any keys with nil values or with an empty map as a value. `partiphify!/2` and `partiphify/2` take a list `l` and an integer `k` and partition `l` into `k` sublists of balanced size. There will always be `k` lists, even if some must be empty. ### Examples: ``` iex> Morphix.partiphify!([:a, :b, :c, :d, :e, :f], 4) [[:c], [:d], [:e, :a], [:f, :b]] iex> Morphix.partiphify!([:a, :b, :c, :d, :e], 4) [[:b], [:c], [:d], [:e, :a]] iex> Morphix.partiphify!([:a, :b, :c, :d], 4) [[:a], [:b], [:c], [:d]] iex> Morphix.partiphify!([:a, :b, :c], 4) [[:a], [:b], [:c], []] ``` """ @spec morphiflat(map()) :: {:ok | :error, map() | String} @spec morphiflat!(map()) :: map() @spec morphify([any], fun()) :: {:ok|:error, map() | String.t} @spec morphify(tuple(), fun()) :: {:ok|:error, map() | String.t} @spec morphify!([any], fun()) :: map() @spec morphify!(tuple(), fun()) :: map() @spec atomorphify(map(), :safe) :: {:ok, map()} @spec atomorphify(map()) :: {:ok, map} @spec atomorphiform(map(), :safe) :: {:ok, map()} @spec atomorphiform(map()) :: {:ok, map} @spec compactify(map()) :: {:ok, map()} | no_return @spec compactify!(map()) :: map() | no_return @spec compactiform!(map()) :: map() | no_return @spec compactiform(map()) :: {:ok, map()} | {:error, %BadMapError{}} @spec partiphify!(list(), integer) :: [list[any]] | no_return @spec partiphify(list(), integer) :: {:ok, [list[any]]} | {:error, term} @doc """ Takes a map and returns a flattend version of that map, discarding any nested keys. ### Examples: ``` iex> Morphix.morphiflat! %{you: "will", youwill: %{be: "discarded"}} %{you: "will", be: "discarded"} ``` """ def morphiflat! map do flattn map end @doc """ Takes a map and returns a flattened version of that map. If the map has nested maps (or the maps nested maps have nested maps, etc.) morphiflat moves all nested key/value pairs to the top level, discarding the original keys. ### Examples: ``` iex> Morphix.morphiflat %{this: %{nested: :map, inner: %{twonested: :map, is: "now flat"}}} {:ok, %{nested: :map, twonested: :map, is: "now flat"}} ``` In the example, the key `:this` is discarded, along with the key `inner`, because they both point to map values. Will return `{:error, is not a Map}` if the input is not a map. ### Examples: ``` iex> Morphix.morphiflat({1,2,3}) {:error, "{1, 2, 3} is not a Map"} ``` """ def morphiflat(map) when is_map map do {:ok, flattn map} rescue exception -> {:error, Exception.message(exception)} end def morphiflat(not_map), do: {:error, "#{inspect(not_map)} is not a Map"} defp flattn map do not_maps = fn({k, v}, acc) -> case is_map v do false -> Map.put_new(acc, k, v) true -> Map.merge(acc, flattn(v)) end end Enum.reduce(map, %{}, not_maps) end @doc """ Takes a map as an argument and returns the same map with string keys converted to atom keys. Does not examine nested maps. ### Examples ``` iex> Morphix.atomorphify(%{"this" => "map", "has" => %{"string" => "keys"}}) {:ok, %{this: "map", has: %{"string" => "keys"}}} iex> Morphix.atomorphify(%{1 => "2", "1" => 2, "one" => :two}) {:ok, %{1 => "2", "1": 2, one: :two}} ``` """ def atomorphify(map) when is_map map do {:ok, atomog(map, &atomize_binary/1)} end @doc """ Takes a map and the `:safe` flag, returns the same map, with string keys converted to existing atoms if possible, and ignored otherwise. Ignores nested maps. ### Examples: ``` iex> :existing_atom iex> Morphix.atomorphify(%{"existing_atom" => "exists", "non_existent_atom" => "does_not", 1 => "is_ignored"}, :safe) {:ok, %{ "non_existent_atom" => "does_not", 1 => "is_ignored", existing_atom: "exists"}} ``` """ def atomorphify(map, :safe) when is_map map do {:ok, (atomog map, &safe_atomize_binary/1)} end @doc """ Takes a map as an argument and returns the same map, with all string keys (including keys in nested maps) converted to atom keys. ### Examples: ``` iex> Morphix.atomorphiform(%{:this => %{map: %{"has" => "a", :nested => "string", :for => %{a: :key}}}, "the" => %{"other" => %{map: :does}}, as: "well"}) {:ok,%{this: %{map: %{has: "a", nested: "string", for: %{a: :key}}}, the: %{other: %{map: :does}}, as: "well"} } iex> Morphix.atomorphiform(%{"this" => ["map", %{"has" => ["a", "list"]}], "inside" => "it"}) {:ok, %{this: ["map", %{has: ["a", "list"]}], inside: "it"}} ``` """ def atomorphiform(map) when is_map map do {:ok, depth_atomog(map, &atomize_binary/1)} end @doc """ Takes a map and the `:safe` flag as arguments and returns `{:ok, map}`, with any strings that are existing atoms converted to atoms, and any strings that are not existing atoms left as strings. Works recursively on embedded maps. ### Examples: ``` iex> [:allowed, :values] iex> map = %{"allowed" => "atoms", "embed" => %{"will" => "convert", "values" => "to atoms"}} iex> Morphix.atomorphiform(map, :safe) {:ok, %{"embed" => %{"will" => "convert", values: "to atoms"}, allowed: "atoms"}} ``` """ def atomorphiform(map, :safe) when is_map map do {:ok, depth_atomog(map, &safe_atomize_binary/1)} end defp process_list_item(item, safe_or_atomize) do cond do is_map item -> depth_atomog(item, safe_or_atomize) is_list item -> Enum.map(item, fn(x) -> process_list_item(x, safe_or_atomize) end) true -> item end end defp depth_atomog(map, safe_or_atomize) do atomkeys = fn({k, v}, acc) -> cond do is_map v -> Map.put_new(acc, safe_or_atomize.(k), depth_atomog(v, safe_or_atomize)) is_list v -> Map.put_new(acc, safe_or_atomize.(k), process_list_item(v, safe_or_atomize)) true -> Map.put_new(acc, safe_or_atomize.(k), v) end end Enum.reduce(map, %{}, atomkeys) end defp atomog(map, safe_or_atomize) do atomkeys = fn({k, v}, acc) -> Map.put_new(acc, safe_or_atomize.(k), v) end Enum.reduce(map, %{}, atomkeys) end defp atomize_binary(value) do if is_binary(value) do String.to_atom(value) else value end end defp safe_atomize_binary(value) do if is_binary(value) do try do String.to_existing_atom(value) rescue _ -> value end else value end end @doc """ Takes a List and a function as arguments and returns `{:ok, Map}`, with the keys of the map the result of applying the function to each item in the list. If the function cannot be applied, will return `{:error, message}` ### Examples ``` iex> Morphix.morphify([[1,2,3], [12], [1,2,3,4]], &Enum.count/1) {:ok, %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]}} iex> Morphix.morphify({[1,2,3], [12], [1,2,3,4]}, &length/1) {:ok, %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]}} iex> Morphix.morphify([1,2], &String.length/1) {:error, "Unable to apply &String.length/1 to each of [1, 2]"} ``` """ def morphify(enum, funct) when is_tuple(enum), do: morphify(Tuple.to_list(enum), funct) def morphify(enum, funct) do {:ok, morphify!(enum, funct)} rescue _ -> {:error, "Unable to apply #{inspect(funct)} to each of #{inspect(enum)}"} end @doc """ Takes a list and a function as arguments and returns a Map, with the keys of the map the result of applying the function to each item in the list. ### Examples ``` iex> Morphix.morphify!([[1,2,3], [12], [1,2,3,4]], &Enum.count/1) %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]} ``` """ def morphify!(enum, funct) when is_tuple(enum), do: morphify!(Tuple.to_list(enum), funct) def morphify!(enum, funct) do Enum.reduce(enum, %{}, fn(x, acc) -> Map.put(acc, funct.(x), x) end) end @doc """ Takes a map and removes keys that have nil values, or are empty maps. ### Examples ``` iex> Morphix.compactify!(%{nil_key: nil, not_nil: "nil"}) %{not_nil: "nil"} iex> Morphix.compactify!(%{empty: %{}, not: "not"}) %{not: "not"} iex> Morphix.compactify!({"not", "a map"}) ** (BadMapError) expected a map, got: {"not", "a map"} ``` """ def compactify!(map) when is_map(map) do map |> Enum.reject(fn({_k, v}) -> is_nil(v) || empty_map(v) end) |> Enum.into(%{}) end def compactify!(not_map) do raise(BadMapError, term: not_map) end @doc """ Takes a map and removes any keys that have nil values. ### Examples ``` iex> Morphix.compactify(%{nil_key: nil, not_nil: "real value"}) {:ok, %{not_nil: "real value"}} iex> Morphix.compactify("won't work") {:error, %BadMapError{term: "won't work"}} ``` """ def compactify(map) do {:ok, compactify!(map)} rescue e -> {:error, e} end @doc """ Removes keys with nil values from nested maps, also eliminates empty maps. ### Examples ``` iex> Morphix.compactiform!(%{nil_nil: nil, not_nil: "a value", nested: %{nil_val: nil, other: "other"}}) %{not_nil: "a value", nested: %{other: "other"}} iex> Morphix.compactiform!(%{nil_nil: nil, not_nil: "a value", nested: %{nil_val: nil, other: "other", nested_empty: %{}}}) %{not_nil: "a value", nested: %{other: "other"}} ``` """ def compactiform!(map) when is_map(map) do compactor = fn({k, v}, acc) -> cond do is_struct(v) -> Map.put_new(acc, k, v) is_map(v) and Enum.empty?(v) -> acc is_map(v) -> Map.put_new(acc, k, compactiform!(v)) is_nil(v) -> acc true -> Map.put_new(acc, k, v) end end map |> Enum.reduce(%{}, compactor) |> compactify! end def compactiform!(not_map) do raise(BadMapError, term: not_map) end @doc """ Removes keys with nil values from maps, handles nested maps and treats empty maps as nil values. ### Examples ``` iex> Morphix.compactiform(%{a: nil, b: "not", c: %{d: nil, e: %{}, f: %{g: "value"}}}) {:ok, %{b: "not", c: %{f: %{g: "value"}}}} iex> Morphix.compactiform(5) {:error, %BadMapError{term: 5}} ``` """ def compactiform(map) do {:ok, compactiform!(map)} rescue e -> {:error, e} end @doc """ Divides a list into k distinct sub-lists, with partitions being as close to the same size as possible ### Examples ``` iex> Morphix.partiphify!([1,2,3,4,5,6], 4) [[3], [4], [5, 1], [6, 2]] iex> Morphix.partiphify!(("abcdefghijklmnop" |> String.split("")), 4) [["e", "f", "g", "h"], ["i", "j", "k", "l"], ["m", "n", "o", "p"], ["", "a", "b", "c", "d"]] ``` """ def partiphify!(list, k) when is_list(list) and is_integer(k) do ceil_div = fn(a, b) -> Float.ceil(a / b) end with chunk_size when chunk_size > 0 <- list |> Enum.count() |> Integer.floor_div(k), true <- (list |> Enum.count() |> Integer.mod(k) |> ceil_div.(chunk_size)) > 0 do list |> into_buckets(k, chunk_size) |> distribute_extra() else 0 -> list = Enum.chunk(list, 1, 1, []) empty_buckets = k - Enum.count(list) Enum.reduce(1..empty_buckets, list, fn(_, acc) -> acc ++ [[]] end) false -> chunk_size = list |> Enum.count() |> Integer.floor_div(k) Enum.chunk(list, chunk_size, chunk_size, []) end end defp into_buckets(list, k, chunk_size) do chunks = Enum.chunk(list, chunk_size, chunk_size, []) extra_buckets = Enum.take(chunks, -(Enum.count(chunks) - k)) k_buckets = chunks -- extra_buckets {extra_buckets, k_buckets} end @doc """ Divides a list into k distinct sub-lists, with partitions being as close to the same size as possible ### Examples ``` iex> Morphix.partiphify([1,2,3,4,5,6], 4) {:ok, [[3], [4], [5, 1], [6, 2]]} iex> Morphix.partiphify(("abcdefghijklmnop" |> String.split("")), 4) {:ok, [["e", "f", "g", "h"], ["i", "j", "k", "l"], ["m", "n", "o", "p"], ["", "a", "b", "c", "d"]]} ``` """ def partiphify(list, k) do {:ok, partiphify!(list, k)} rescue e -> {:error, e} end defp distribute(list, buckets) do Enum.reduce(list, buckets, fn(item, buckets) -> [current_bucket | rest_of_buckets] = buckets new_bucket = [item | current_bucket] rest_of_buckets ++ [new_bucket] end) end defp distribute_extra({lists, buckets}) do with false <- Enum.empty?(lists) do [current_list | rest] = lists new_buckets = distribute(current_list, buckets) distribute_extra({rest, new_buckets}) else _ -> buckets end end defp empty_map(map) do is_map(map) && (not Map.has_key?(map, :__struct__)) && Enum.empty?(map) end defp is_struct(s), do: is_map(s) and Map.has_key?(s, :__struct__) end