One annoying detail about web forms is that on submission (unless you are using changesets) all the values come back as strings. This forces you to have to handle the correct formats before, say, passing those params into a service module to execute some action.
To simplify this process, i recently created a simple guesser function that evaluates the values of a map, namely booleans, integers, floats, dates, and datetimes. It is recursive and can go into lists as well. Check the examples:
@doc """
Function to try to guess the type of a variable and return the guessed value. Some
applications might include converting a parameter map coming from a form (all strings)
into a proper map with typed values, for instance:
%{foo: "123", bar: "true", foobar: "2020-12-13"}
would become:
%{foo: 123, bar: true, foobar: ~D[2020-12-13]}
The function supports simple values (a string, for instance), is also recursive into
lists and maps and, if it can't figure out the type, it returns the original value.
Will also trim the values before testing so `" 123"`, for instance, becomes `123`.
## Examples
## Simple types
iex> to_typed("")
""
iex> to_typed("123")
123
iex> to_typed(" 456")
456
iex> to_typed("-123")
-123
iex> to_typed("123.345")
123.345
iex> to_typed("-12.3")
-12.3
iex> to_typed("true")
true
iex> to_typed(" true ")
true
iex> to_typed("false")
false
iex> to_typed("2020-12-13")
~D[2020-12-13]
iex> to_typed("2015-01-23T23:50:07Z")
~U[2015-01-23 23:50:07Z]
iex> to_typed("2015-01-23T23:50:07.123+02:30")
~U[2015-01-23 21:20:07.123Z]
## Maps
iex> to_typed(%{})
%{}
iex> to_typed(%{another_integer: "5345345435"})
%{another_integer: 5345345435}
iex> to_typed(%{another_map: %{foo: "true"}})
%{another_map: %{foo: true}}
iex> to_typed(%{a_list: ["foo", "true"]})
%{a_list: ["foo", true]}
## Lists
iex> to_typed([])
[]
iex> to_typed(["123"])
[123]
iex> to_typed(["12.3"])
[12.3]
iex> to_typed(["true", "false"])
[true, false]
iex> to_typed(["true", ["false"]])
[true, [false]]
iex> to_typed([%{foo: "1"}, %{foo: "2.5"}])
[%{foo: 1}, %{foo: 2.5}]
iex> to_typed(["bla", "bla", "2015-01-23T23:50:07Z", ["12345", %{cool: "true"}]])
["bla", "bla", ~U[2015-01-23 23:50:07Z], [12345, %{cool: true}]]
## Other
iex> to_typed(nil)
nil
iex> to_typed(123)
123
iex> to_typed("actually_a_string")
"actually_a_string"
"""
@spec to_typed(any()) :: any()
def to_typed(map) when is_map(map) do
map
|> Enum.map(fn {key, value} ->
value =
cond do
is_map(value) -> to_typed(value)
is_list(value) -> to_typed(value)
true -> to_typed(value)
end
{key, value}
end)
|> Map.new()
end
def to_typed(list) when is_list(list) do
Enum.map(list, fn value ->
cond do
is_map(value) or is_list(value) -> to_typed(value)
is_binary(value) -> to_typed(value)
true -> value
end
end)
end
def to_typed(value) when is_binary(value) do
value = String.trim(value)
cond do
value == "true" ->
true
value == "false" ->
false
value =~ ~r/\A\d{4}-\d{2}-\d{2}\z/ ->
value |> Timex.parse!("{ISOdate}") |> Timex.to_date()
value =~ ~r/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ ->
value |> Timex.parse!("{ISO:Extended}") |> Timex.to_datetime()
value =~ ~r/\A-?\d+\.\d+\z/ ->
String.to_float(value)
value =~ ~r/\A-?\d+\z/ ->
String.to_integer(value)
true ->
value
end
end
def to_typed(value), do: value
Hope you find it useful.
Happy Elixir'ing...