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(<<a, b, t::binary>>)
when is_upper(a) and is_lower(b),
do: <<to_lower(a), b>> <> 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(<<a, b, t::binary>>)
when is_separator(a),
do: <<to_upper(b)>> <> to_camel_case(t)
def to_camel_case(<<h, t::binary>>), do: <<h>> <> 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(<<a, b, t::binary>>)
when is_upper(a) and is_lower(b),
do: <<to_lower(a), b>> <> 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(<<h, t::binary>>, separator)
when is_separator(h),
do: <<separator>> <> do_to_separator_case(t, separator)
defp do_to_separator_case(<<a, b, t::binary>>, separator)
when is_lower(a) and is_upper(b),
do: <<a, separator, to_lower(b)>> <> do_to_separator_case(t, separator)
defp do_to_separator_case(<<h, t::binary>>, separator),
do: <<h>> <> 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