robinvdvleuten

robinvdvleuten

Better parsing of a duration string

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

Most Liked

OvermindDL1

OvermindDL1

That’s precisely what nimble_parsec is for. ^.^

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.

alco

alco

# 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
robinvdvleuten

robinvdvleuten

Thanks @alco! Wasn’t aware of doctest yet, so even learned that one along the way :sweat_smile:

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

We're in Beta

About us Mission Statement