defmodule Datix.Time do @moduledoc """ A `Time` parser using `Calendar.strftime/3` format-string. """ alias Datix.ValidationError @doc """ Parses a time string according to the given `format`. See the `Calendar.strftime/3` documentation for how to specify a format string. ## Options * `:calendar` - the calendar to build the `Time`, defaults to `Calendar.ISO` * `:preferred_time` - a string for the preferred format to show times, it can't contain the `%X` format and defaults to `"%H:%M:%S"` if the option is not received * `:am_pm_names` - a keyword list with the names of the period of the day, defaults to `[am: "am", pm: "pm"]`. Missing values will be set to minimum. ## Examples iex> Datix.Time.parse("11:12:55", "%X") {:ok, ~T[11:12:55]} iex> format = Datix.compile!("%X") iex> Datix.Time.parse("11:12:55", format) {:ok, ~T[11:12:55]} iex> Datix.Time.parse("10 PM", "%I %p") {:ok, ~T[22:00:00]} """ @spec parse(String.t(), String.t() | Datix.compiled(), list()) :: {:ok, Time.t()} | {:error, Datix.FormatStringError.t() | Datix.ValidationError.t() | Datix.ParseError.t() | Datix.OptionError.t()} def parse(time_str, format, opts \\ []) do with {:ok, data} <- Datix.strptime(time_str, format, sweep(opts)) do new(data, opts) end end @doc """ Parses a date string according to the given `format`, erroring out for invalid arguments. ## Options Accepts the same options as listed for `parse/3`. """ @spec parse!(String.t(), String.t() | Datix.compiled(), list()) :: Time.t() def parse!(time_str, format, opts \\ []) do case parse(time_str, format, opts) do {:ok, time} -> time {:error, error} when is_exception(error) -> raise error end end @doc false def new(%{hour: hour, hour_12: hour_12} = data, opts) do with {:ok, hour_24} <- to_hour_24(hour_12, Map.get(data, :am_pm)) do case hour == hour_24 do true -> data |> Map.delete(:hour_12) |> new(opts) false -> {:error, %ValidationError{reason: :invalid_time, module: __MODULE__}} end end end def new(%{hour: h, minute: m, second: s, microsecond: ms}, opts) do case Time.new(h, m, s, microsecond(ms), Datix.calendar(opts)) do {:ok, time} -> {:ok, time} {:error, :invalid_time} -> {:error, %ValidationError{reason: :invalid_time, module: __MODULE__}} end end def new(%{hour_12: h_12, minute: m, second: s, microsecond: ms} = data, opts) do with {:ok, h} <- to_hour_24(h_12, Map.get(data, :am_pm)) do case Time.new(h, m, s, microsecond(ms), Datix.calendar(opts)) do {:ok, time} -> {:ok, time} {:error, :invalid_time} -> {:error, %ValidationError{reason: :invalid_time, module: __MODULE__}} end end end def new(data, opts), do: data |> Datix.assume(Time) |> new(opts) defp to_hour_24(_hour_12, nil), do: {:error, %ValidationError{reason: :invalid_time, module: __MODULE__}} defp to_hour_24(12, :am), do: {:ok, 0} defp to_hour_24(12, :pm), do: {:ok, 12} defp to_hour_24(hour_12, :am), do: {:ok, hour_12} defp to_hour_24(hour_12, :pm), do: {:ok, hour_12 + 12} defp microsecond(ms) when is_tuple(ms), do: ms defp microsecond(ms) do digits = Integer.digits(ms) precision = length(digits) new_ms = Integer.undigits(digits ++ List.duplicate(0, max(0, 6 - precision))) {new_ms, precision} end defp sweep(opts), do: Keyword.delete(opts, :calendar) end