defmodule ConvCase do @moduledoc """ Functions to convert strings, atoms and map keys between `camelCase`, `snake_case` and `kebab-case`. Currently this functions do not support UTF-8. If this package fits not your requirements then take a look here: * [Macro.camelize/1](https://hexdocs.pm/elixir/Macro.html#camelize/1) and [Macro.underscore/1](https://hexdocs.pm/elixir/Macro.html#underscore/1) * [ReCase](https://github.com/sobolevn/recase) helps you to convert a string from any case to any case. * [ProperCase](https://github.com/johnnyji/proper_case) an Elixir library that converts keys in maps between snake_case and camel_case. """ @underscore ?_ @hyphen ?- @separator [@underscore, @hyphen] defguardp is_upper(char) when char >= ?A and char <= ?Z defguardp is_lower(char) when not (char >= ?A and char <= ?Z) defguardp is_separator(char) when char in @separator @doc """ Converts `camelCase` and `kebab-case` into `snake_case`. For strings, the function returns the converted string. ## Examples iex> ConvCase.to_snake_case("fooBar") "foo_bar" iex> ConvCase.to_snake_case("foo-bar") "foo_bar" For atoms, the function returns the converted atom. `String.to_existing_atom/1` is used to create "new" atoms. iex> ConvCase.to_snake_case(:fooBar) :foo_bar For lists, the function returns a list with converted values. iex> ConvCase.to_snake_case(["fooBar", "foo-bar"]) ["foo_bar", "foo_bar"] For tuples, the function returns a tuple with converted values. iex> ConvCase.to_snake_case({"fooBar", "foo-bar"}) {"foo_bar", "foo_bar"} For maps, the function returns a map with converted keys. The type of the key will not be changed. New atoms are generated by `String.to_existing_atom/1`. The keys of nested maps are also converted. iex> ConvCase.to_snake_case(%{fooBar: %{"foo-bar" => "foo-bar"}}) %{foo_bar: %{"foo_bar" => "foo-bar"}} For other types, the function returns the given value. iex> ConvCase.to_snake_case(42) 42 """ @spec to_snake_case(any) :: any def to_snake_case(string) def to_snake_case(""), do: "" def to_snake_case(atom) when is_atom(atom), do: atom |> Atom.to_string() |> to_snake_case() |> String.to_existing_atom() def to_snake_case(strings) when is_list(strings), do: Enum.map(strings, &to_snake_case/1) def to_snake_case(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> Enum.map(&to_snake_case/1) |> List.to_tuple() def to_snake_case(map) when is_map(map), do: convert_map(map, &to_snake_case/1) def to_snake_case(<>) when is_upper(a) and is_lower(b), do: <> <> do_to_separator_case(t, @underscore) def to_snake_case(string) when is_binary(string), do: do_to_separator_case(string, @underscore) def to_snake_case(any), do: any @doc """ Converts `snake_case` and `kebab-case` into `camelCase`. For strings, the function returns the converted string. ## Examples iex> ConvCase.to_camel_case("foo_bar") "fooBar" iex> ConvCase.to_camel_case("foo-bar") "fooBar" For atoms, the function returns the converted atom. This function used `String.to_existing_atom/1`. ## Examples iex> ConvCase.to_camel_case(:foo_bar) :fooBar For lists, the function returns a list with converted values. ## Examples iex> ConvCase.to_camel_case(["foo_bar", "foo-bar"]) ["fooBar", "fooBar"] For tuples, the function returns a tuple with converted values. ## Examples iex> ConvCase.to_camel_case({"foo_bar", "foo-bar"}) {"fooBar", "fooBar"} For maps, the function returns a map with converted keys. The type of the key will not be changed. New atoms are generated by `String.to_existing_atom/1`. Keys of nested maps are converted too. ## Examples iex> ConvCase.to_camel_case(%{foo_bar: %{"foo-bar" => "foo-bar"}}) %{fooBar: %{"fooBar" => "foo-bar"}} For other types, the function returns the given value. ## Examples iex> ConvCase.to_camel_case(42) 42 """ @spec to_camel_case(any) :: any def to_camel_case(value) def to_camel_case(""), do: "" def to_camel_case(atom) when is_atom(atom), do: atom |> Atom.to_string() |> to_camel_case() |> String.to_existing_atom() def to_camel_case(strings) when is_list(strings), do: Enum.map(strings, &to_camel_case/1) def to_camel_case(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> Enum.map(&to_camel_case/1) |> List.to_tuple() def to_camel_case(map) when is_map(map), do: convert_map(map, &to_camel_case/1) def to_camel_case(<>) when is_separator(a), do: <> <> to_camel_case(t) def to_camel_case(<>), do: <> <> to_camel_case(t) def to_camel_case(any), do: any @doc """ Converts `snake_case` and `camelCase` into `kebab-case`. For strings, the function returns the converted string. ## Examples iex> ConvCase.to_kebab_case("foo_bar") "foo-bar" iex> ConvCase.to_kebab_case("fooBar") "foo-bar" For atoms, the function returns the converted atom. This function used `String.to_existing_atom/1`. ## Examples iex> ConvCase.to_kebab_case(:foo_bar) :"foo-bar" For lists, the function returns a list with converted values. ## Examples iex> ConvCase.to_kebab_case(["foo_bar", "fooBar"]) ["foo-bar", "foo-bar"] For tuples, the function returns a tuple with converted values. ## Examples iex> ConvCase.to_kebab_case({"foo_bar", "fooBar"}) {"foo-bar", "foo-bar"} For maps, the function returns a map with converted keys. The type of the key will not be changed. New atoms are generated by `String.to_existing_atom/1`. Keys of nested maps are converted too. ## Examples iex> ConvCase.to_kebab_case(%{foo_bar: %{"fooBar" => "fooBar"}}) %{"foo-bar": %{"foo-bar" => "fooBar"}} For other types, the function returns the given value. ## Examples iex> ConvCase.to_kebab_case(42) 42 """ @spec to_kebab_case(any) :: any def to_kebab_case(value) def to_kebab_case(""), do: "" def to_kebab_case(atom) when is_atom(atom), do: atom |> Atom.to_string() |> to_kebab_case() |> String.to_existing_atom() def to_kebab_case(strings) when is_list(strings), do: Enum.map(strings, &to_kebab_case/1) def to_kebab_case(tuple) when is_tuple(tuple), do: tuple |> Tuple.to_list() |> Enum.map(&to_kebab_case/1) |> List.to_tuple() def to_kebab_case(map) when is_map(map), do: convert_map(map, &to_kebab_case/1) def to_kebab_case(<>) when is_upper(a) and is_lower(b), do: <> <> do_to_separator_case(t, @hyphen) def to_kebab_case(string) when is_binary(string), do: do_to_separator_case(string, @hyphen) def to_kebab_case(any), do: any # Convert string with given separator. defp do_to_separator_case("", _separator), do: "" defp do_to_separator_case(<>, separator) when is_separator(h), do: <> <> do_to_separator_case(t, separator) defp do_to_separator_case(<>, separator) when is_lower(a) and is_upper(b), do: <> <> do_to_separator_case(t, separator) defp do_to_separator_case(<>, separator), do: <> <> do_to_separator_case(t, separator) # Convert map keys with the given converter. defp convert_map(%{__struct__: _} = struct, _), do: struct defp convert_map(map, converter) when is_map(map) do for {key, value} <- map, into: %{}, do: {convert_key(key, converter), convert_map(value, converter)} end defp convert_map(list, converter) when is_list(list), do: Enum.map(list, &convert_map(&1, converter)) defp convert_map(map, _converter), do: map # Convert key with the given converter. defp convert_key(key, converter) when is_atom(key), do: key |> converter.() defp convert_key(key, converter), do: converter.(key) # Convert a lowercase character into an uppercase character. defp to_upper(char), do: char - 32 # Convert an uppercase character into a lowercase character. defp to_lower(char), do: char + 32 end