ISO8601 duration strings (e.g., "P3Y6M4DT12H30M5S") are a standardized way to represent time durations. While Elixir has strong support for ISO8601 date and time formats via the Date, Time, and DateTime modules, parsing durations requires a bit more work.

Why it matters

You might encounter ISO8601 durations when working with APIs (like Google Calendar or video metadata) that encode durations in this format. To use them effectively in Elixir, you'll want to parse them into something usable like a map or seconds.

Using Timex

The Timex library provides built-in support for parsing ISO8601 durations.

Installation

Add Timex to your mix.exs:

def deps do
  [
    {:timex, "~> 3.7"}
  ]
end

Then run:

mix deps.get

Parsing a Duration

iex> Timex.Duration.parse("P1DT2H3M4S")
{:ok, #<Duration(P1DT2H3M4S)>}

You can convert it to a human-readable format or manipulate it:

{:ok, duration} = Timex.Duration.parse("PT30M")
Timex.Duration.to_minutes(duration)
# => 30.0
Timex.Format.Duration.Formatter.format(d)
# => PT30M

You can also do things like this:

{:ok, d} = Timex.Duration.parse("P3.5M")
d |> Timex.Duration.to_days() |> Timex.Duration.from_days()
#<Duration(P3M15D)>

Manual parsing (regex-based)

If you prefer not to use a dependency, you can parse the string yourself:

defmodule DurationParser do
  @regex ~r/P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?)?/

  def parse(iso8601) do
    case Regex.named_captures(@regex, iso8601) do
      nil -> :error
      captures ->
        Enum.reduce(captures, 0, fn
          {_, nil}, acc -> acc
          {"days", v}, acc -> acc + String.to_integer(v) * 86400
          {"hours", v}, acc -> acc + String.to_integer(v) * 3600
          {"minutes", v}, acc -> acc + String.to_integer(v) * 60
          {"seconds", v}, acc -> acc + String.to_integer(v)
        end)
    end
  end
end

DurationParser.parse("P1DT2H3M4S")
# => 93784