Skip to content

Instantly share code, notes, and snippets.

@mindreframer
Forked from rcdilorenzo/duration.ex
Created April 3, 2016 08:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mindreframer/dc08caddffd99a586f221938324efb2f to your computer and use it in GitHub Desktop.
Save mindreframer/dc08caddffd99a586f221938324efb2f to your computer and use it in GitHub Desktop.
A simple construction of a duration time (e.g. "01:30" or "01:30:23") as a custom ecto type
defmodule Duration do
@moduledoc """
This duration module parses and formats strings
for a time duration in hours and minutes and
optionally seconds (e.g. 01:00 for an hour,
00:01:10 for one minute and ten seconds).
"""
@match ~r/^(?<hour>\d{1,2}):(?<min>\d{1,2}):?(?<sec>\d{0,2})$/
def parse(string) do
case Regex.named_captures(@match, string) do
nil -> :error
%{"hour" => hour_str, "min" => min_str, "sec" => sec_str} ->
with {:ok, hours} <- parse_integer(hour_str),
{:ok, minutes} <- parse_integer(min_str),
{:ok, seconds} <- parse_integer(sec_str),
do: validate(hours, minutes, seconds)
end
end
def format(integer) when integer >= 0 and integer < 86_400 do
hours = div(integer, 3600)
minutes = div(integer - (hours * 3600), 60)
seconds = integer - (hours * 3600) - (minutes * 60)
case seconds do
0 -> {:ok, "#{pad(hours)}:#{pad(minutes)}"}
_ -> {:ok, "#{pad(hours)}:#{pad(minutes)}:#{pad(seconds)}"}
end
end
def format(_), do: :error
defp pad(integer) do
to_string(integer) |> String.rjust(2, ?0)
end
defp parse_integer(""), do: {:ok, 0}
defp parse_integer(string) when is_binary(string) do
case Integer.parse(string) do
{value, _} -> {:ok, value}
:error -> :error
end
end
defp validate(hours, minutes, seconds) do
cond do
hours < 24 and minutes < 60 and seconds < 60 ->
{:ok, (hours * 3600) + (minutes * 60) + seconds}
true -> :error
end
end
end
defmodule DurationTest do
use ExUnit.Case
test "validating hours" do
assert Duration.parse("24:00") == :error
assert Duration.parse("-4:00") == :error
assert Duration.parse("100:00") == :error
assert Duration.parse(":") == :error
refute Duration.parse("23:00") == :error
refute Duration.parse("0:0") == :error
end
test "validating minutes" do
assert Duration.parse("00:60") == :error
assert Duration.parse("00:-6") == :error
assert Duration.parse("00:-1:00") == :error
assert Duration.parse("00:60:00") == :error
refute Duration.parse("00:59") == :error
refute Duration.parse("00:59:00") == :error
refute Duration.parse("00:00:00") == :error
end
test "validating seconds" do
assert Duration.parse("00:00:60") == :error
assert Duration.parse("00:00:-9") == :error
refute Duration.parse("00:00:34") == :error
end
test "parsing hours and minutes" do
assert Duration.parse("01:59") == {:ok, 7140}
assert Duration.parse("20:00") == {:ok, 72_000}
assert Duration.parse("00:10") == {:ok, 600}
end
test "parsing hours and minutes and seconds" do
assert Duration.parse("00:00:01") == {:ok, 1}
assert Duration.parse("01:10:05") == {:ok, 4205}
end
test "validate format" do
assert Duration.format(86_400) == :error
assert Duration.format(-1) == :error
refute Duration.format(1_000) == :error
end
test "formatting" do
assert Duration.format(7140) == {:ok, "01:59"}
assert Duration.format(7150) == {:ok, "01:59:10"}
assert Duration.format(45_296) == {:ok, "12:34:56"}
end
end
defmodule MyApp.Type.Duration do
@behaviour Ecto.Type
def type, do: :integer
def cast(string) when is_binary(string) do
Duration.parse(string)
end
def cast(integer) when integer >= 0 and integer < 86_400 do
{:ok, integer}
end
def cast(_), do: :error
def load(integer) when is_integer(integer) do
Duration.format(integer)
end
def dump(integer) when integer >= 0 and integer < 86_400 do
{:ok, integer}
end
def dump(_), do: :error
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment