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. """ @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. This function used `String.to_existing_atom/1`. ## Examples iex> ConvCase.to_snake_case(:fooBar) :foo_bar For lists, the function returns a list with converted values. ## Examples 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`. Keys of nested maps are converted too. ## Examples 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. ## Examples 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(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 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(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 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(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(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