defmodule Transmogrify.As do
@moduledoc """
Polymorphic data transforms — accepts many input forms and tries to put
them into the expected form.
Easiest use:
```elixir
iex> import Transmogrify.As
iex> as_int("10")
{:ok, 10}
iex> as_int!(10.0)
10
```
"""
@invalid_error :error
##############################################################################
@doc ~S"""
Accept strings, floats, and ints, and assure the result is either
an int (parsed if necessary) or an error.
```elixir
iex> as_int(10)
{:ok, 10}
iex> as_int(10.0)
{:ok, 10}
iex> as_int("10")
{:ok, 10}
iex> as_int("tardis")
:error
iex> as_int(:foo)
:error
```
"""
def as_int(arg) when is_integer(arg), do: {:ok, arg}
def as_int(arg) when is_float(arg), do: {:ok, Kernel.trunc(arg)}
def as_int(arg) when is_binary(arg) do
{:ok, String.to_integer(arg)}
rescue
ArgumentError -> @invalid_error
end
def as_int(_), do: @invalid_error
@doc ~S"""
Accept strings, floats, and ints, and assure the result is either
an int (parsed if necessary) or return the specified default (which
is `0` if unspecified)
```elixir
iex> as_int!(10)
10
iex> as_int!(10.0)
10
iex> as_int!("10")
10
iex> as_int!("tardis", 9)
9
iex> as_int!(:foo, 8)
8
```
"""
def as_int!(arg, default \\ 0) do
case as_int(arg) do
{:ok, x} -> x
@invalid_error -> default
end
end
##############################################################################
@doc ~S"""
Accept strings, floats, and ints, and assure the result is either
an float (parsed if necessary) or an error.
```elixir
iex> as_float(10.52)
{:ok, 10.52}
iex> as_float(10)
{:ok, 10.0}
iex> as_float("10.5")
{:ok, 10.5}
iex> as_float(".55")
{:ok, 0.55}
iex> as_float("tardis")
:error
```
"""
def as_float(data) when is_float(data), do: {:ok, data}
def as_float(data) when is_integer(data), do: {:ok, data / 1}
def as_float(<<?., data::binary>>) do
{:ok, String.to_float("0.#{data}")}
rescue
ArgumentError -> @invalid_error
end
def as_float(data) when is_binary(data) do
{:ok, String.to_float(data)}
rescue
ArgumentError -> @invalid_error
end
def as_float(_), do: @invalid_error
@doc ~S"""
Accept strings, floats, and ints, and assure the result is either
an float (parsed if necessary) or return the specified default (which
is `0.0` if unspecified)
```elixir
iex> as_float!(10.0)
10.0
iex> as_float!(10)
10.0
iex> as_float!("10.0")
10.0
iex> as_float!("tardis", 9.0)
9.0
```
"""
def as_float!(arg, default \\ 0.0) do
case as_float(arg) do
{:ok, num} -> num
@invalid_error -> default
end
end
##############################################################################
@doc ~S"""
Accept strings, floats, and ints, and assure the result is either
a float or int (parsed if necessary) or an error.
```elixir
iex> as_number(10.52)
{:ok, 10.52}
iex> as_number(10)
{:ok, 10}
iex> as_number("10.5")
{:ok, 10.5}
iex> as_number(".55")
{:ok, 0.55}
iex> as_number("tardis")
:error
```
"""
def as_number(arg) when is_number(arg), do: {:ok, arg}
def as_number(arg) when is_integer(arg), do: {:ok, arg / 1}
def as_number(<<?., data::binary>>) do
{:ok, String.to_float("0.#{data}")}
rescue
ArgumentError -> @invalid_error
end
# TODO: benchmark which is faster: Scanning for a '.' first, or catching
# the exception and handling
def as_number(data) when is_binary(data) do
{:ok, String.to_float(data)}
rescue
ArgumentError -> as_int(data)
end
def as_number(_), do: @invalid_error
@doc ~S"""
Accept strings, floats, and ints, and assure the result is either
a float or int (parsed if necessary) or return the specified default (which
is `0` if unspecified)
```elixir
iex> as_number!(10.0)
10.0
iex> as_number!(10)
10
iex> as_number!("10.0")
10.0
iex> as_number!("tardis", 9.0)
9.0
```
"""
def as_number!(arg, default \\ 0.0) do
case as_number(arg) do
{:ok, num} -> num
@invalid_error -> default
end
end
##############################################################################
@doc """
Accept various data forms and assures they are atoms.
WARNING: Make sure inputs being called are not using user-submitted data;
or this may exhaust the atoms table.
```elixir
iex> as_atom("long ugly thing prolly")
{:ok, :"long ugly thing prolly"}
iex> as_atom("as_atom")
{:ok, :as_atom}
iex> as_atom(:atom)
{:ok, :atom}
iex> as_atom(["as_atom", "another"])
{:ok, [:as_atom, :another]}
iex> as_atom('test')
{:ok, :test}
iex> as_atom(:atom)
{:ok, :atom}
```
"""
def as_atom(key) when is_atom(key), do: {:ok, key}
def as_atom(str) when is_binary(str), do: {:ok, String.to_atom(str)}
# if first elem is an int, assume charlist...
def as_atom([x | _] = list) when is_integer(x), do: as_atom(to_string(list))
def as_atom(list) when is_list(list), do: {:ok, Enum.map(list, &as_atom!/1)}
def as_atom(_), do: @invalid_error
@doc """
Accept various data forms and assures they are atoms.
WARNING: Make sure inputs being called are not using user-submitted data;
or this may exhaust the atoms table.
```elixir
iex> as_atom!("long ugly thing prolly")
:"long ugly thing prolly"
iex> as_atom!("as_atom")
:as_atom
iex> as_atom!(:atom)
:atom
iex> as_atom!(["as_atom", "another"])
[:as_atom, :another]
iex> as_atom!('test')
:test
iex> as_atom!(:atom)
:atom
iex> as_atom!({:oops})
** (ArgumentError) argument error
```
"""
def as_atom!(val) do
case as_atom(val) do
{:ok, val} -> val
@invalid_error -> raise ArgumentError
end
end
##############################################################################
@doc """
Accept various data forms and assures they are atoms (from the existing
atoms table). See String.to_existing_atom/1 for more details.
```elixir
iex> as_existing_atom("long ugly thing prolly")
{:ok, :"long ugly thing prolly"}
iex> as_existing_atom("non_existing_atom")
:error
iex> as_existing_atom("as_existing_atom")
{:ok, :as_existing_atom}
iex> as_existing_atom(:atom)
{:ok, :atom}
iex> as_existing_atom(["as_existing_atom", "another"])
{:ok, [:as_existing_atom, :another]}
iex> as_existing_atom(:atom)
{:ok, :atom}
```
"""
def as_existing_atom(key) when is_atom(key), do: {:ok, key}
def as_existing_atom(str) when is_binary(str) do
{:ok, String.to_existing_atom(str)}
rescue
ArgumentError -> @invalid_error
end
# if first elem is an int, assume charlist...
def as_existing_atom([x | _] = list) when is_integer(x), do: as_existing_atom(to_string(list))
def as_existing_atom(list) when is_list(list), do: {:ok, Enum.map(list, &as_existing_atom!/1)}
def as_existing_atom(_), do: @invalid_error
@doc """
Accept various data forms and assures they are atoms.
WARNING: Make sure inputs being called are not using user-submitted data;
or this may exhaust the atoms table.
```elixir
iex> as_existing_atom!("long ugly thing prolly")
:"long ugly thing prolly"
iex> as_existing_atom!("as_existing_atom")
:as_existing_atom
iex> as_existing_atom!(:atom)
:atom
iex> as_existing_atom!(["as_existing_atom", "another"])
[:as_existing_atom, :another]
iex> as_existing_atom!(:atom)
:atom
iex> as_existing_atom!({:oops})
** (ArgumentError) argument error
iex> as_existing_atom!("non_existing_atom")
** (ArgumentError) argument error
```
"""
def as_existing_atom!(val) do
case as_existing_atom(val) do
{:ok, val} -> val
@invalid_error -> raise ArgumentError
end
end
end