I have a form where users are allowed to enter either something looking like a float or something looking a duration as string. For example “02:15”, “0,25” or “8.00”. This would then be converted to an integer representing the amount of seconds through a custom Ecto duration type.
I have the following pipeline in Elixir what seems to be doing what I would like. But somehow the code feels a bit lanky. Does someone has an idea to improve it?
def parse(value) do
if String.match?(value, ~r/^\d+([:,.]?\d*)/) do
parsed =
value
|> String.replace(",", ".")
|> String.split(":", parts: 2, trim: true)
|> Enum.map(&Float.parse/1)
|> Enum.map(&elem(&1, 0))
|> Enum.zip([3600, 60])
|> Enum.map(fn {value, factor} -> value * factor end)
|> Enum.reduce(0, &(&1 + &2))
|> round()
{:ok, parsed}
else
:error
end
end
However I’d probably just be lazy and swap the . and , with : then just run it through Timex.parse. The Float.parse and elem calls on that are definitely a bit flimsy. but they could be cleaned up. The zip is an interesting method of setting up the additive part though.
# lib/parse_interval.ex
defmodule ParseInterval do
@doc """
Parse a given string as either a time interval or a fractional number of hours and return the equivalent number of
seconds.
## Examples
iex> ParseInterval.parse("02:15")
8100
iex> ParseInterval.parse("0,25")
900
iex> ParseInterval.parse("8.00")
28800
iex> ParseInterval.parse("8.13:05")
{:error, :invalid_format}
"""
def parse(str) do
with :error <- try_parse_as_time(str),
:error <- try_parse_as_number(str) do
{:error, :invalid_format}
end
end
###
defp try_parse_as_time(str) do
case String.split(str, ":") do
[hours, minutes] -> as_time(hours, minutes)
_ -> :error
end
end
defp as_time(hours_str, minutes_str) do
with {:ok, hours} <- parse_integer(hours_str),
{:ok, minutes} <- parse_integer(minutes_str) do
hours * 3600 + minutes * 60
end
end
defp parse_integer(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> :error
end
end
defp try_parse_as_number(str) do
case str |> String.replace(",", ".") |> Float.parse() do
{num, ""} -> trunc(num * 3600)
_ -> :error
end
end
end
# test/parse_interval_test.exs
defmodule ParseIntervalTest do
use ExUnit.Case
doctest ParseInterval
end
$ mix test
....
Finished in 0.03 seconds
4 doctests, 0 failures