Skip to content

Instantly share code, notes, and snippets.

@srevenant
Last active October 10, 2019 19:41
Show Gist options
  • Save srevenant/488c0152bc1556cdd5f26518505e9bea to your computer and use it in GitHub Desktop.
Save srevenant/488c0152bc1556cdd5f26518505e9bea to your computer and use it in GitHub Desktop.
Time bits for use in Elixir. Sometime, it's easier to just fall back to posix time... LMK if you find better ways to do this!
defmodule Utils.Time do
import Utils.Types, only: [str_to_int!: 1, str_to_float: 1]
import Utils.Enum, only: [enum_rx: 2]
# note for future
# https://hexdocs.pm/nimble_parsec/NimbleParsec.html
def iso_time_range(input) when is_binary(input) do
case String.split(input, "/") do
[stime, etime] ->
iso_time_range(stime, etime)
_other ->
{:error, "Input #{input} is not a valid ISO time range"}
end
end
def iso_time_range(stime, etime) when is_binary(stime) and is_binary(etime) do
with {:ok, d_stime = %DateTime{}, _offset} <- DateTime.from_iso8601(stime),
{:ok, d_etime = %DateTime{}, _offset} <- DateTime.from_iso8601(etime) do
{:ok, d_stime, d_etime, Timex.to_unix(d_etime) - Timex.to_unix(d_stime)}
else
%MatchError{} = e ->
IO.inspect(e)
{:error, "Unable to process time #{stime}/#{etime}"}
end
end
@doc """
Iterate a map and merge string & atom keys into just atoms.
Not recursive, only top level.
Behavior with mixed keys being merged is not guaranteed, as maps are not always
ordered.
## Examples
iex> {:ok, %DateTime{}, %DateTime{}, elapsed} = time_range("20 min")
iex> elapsed
1200
"""
def time_range(input) do
time_range(input, DateTime.utc_now())
end
def time_range(input, reference = %DateTime{}) do
# regex list of supported variants
time_rxs = [
{~r/^\s*((\d+):(\d+))\s*((p|a)m?)?\s*$/i,
fn match, _time ->
stime = time_at_today(match, reference)
{stime, stime}
end},
{~r/^\s*((\d+):(\d+))\s*((p|a)m?)?\s*-\s*(.+)\s*$/i,
fn match, _time ->
stime = time_at_today(Enum.slice(match, 0, 6), reference)
case time_at_today(Enum.at(match, 6), reference) do
nil ->
nil
%DateTime{} = etime ->
etime =
if etime < stime do
# assume 12hrs left off
Timex.shift(etime, hours: 12)
else
etime
end
{stime, etime}
end
end},
{~r/^\s*((\d+):(\d+))\s*((p|a)m?)?\s*\+\s*(.+)\s*$/i,
fn match, _time ->
stime = time_at_today(Enum.slice(match, 0, 6), reference)
case time_duration(Enum.at(match, 6)) do
nil ->
nil
mins ->
{stime, Timex.shift(stime, minutes: mins)}
end
end},
{~r/^\s*([0-9.]+)\s*([a-z]+)?$/i,
fn match, _time ->
case time_duration(match) do
nil ->
nil
mins ->
{Timex.shift(reference, minutes: -mins), reference}
end
end}
]
case enum_rx(time_rxs, input) do
nil ->
{:error, "Sorry, I don't understand the time range #{inspect(input)}"}
{stime, etime} ->
{:ok, stime, etime, Timex.to_unix(etime) - Timex.to_unix(stime)}
end
end
def hr_to_zulu(hr, ""), do: hr
def hr_to_zulu(hr, "A"), do: hr
def hr_to_zulu(hr, "P") do
hr + 12
end
def time_at_today(input, reference) when is_binary(input),
do: time_at_today(Regex.run(~r/^\s*((\d+):(\d+))\s*((p|a)m?)?$/i, input), reference)
def time_at_today(nil, _reference), do: nil
def time_at_today(regmatch, reference) do
{mhr, mmin} =
case regmatch do
[_, _, hr, min] ->
{str_to_int!(hr), str_to_int!(min)}
[_, _, hr, min, _ampm, ap] ->
{str_to_int!(hr) |> hr_to_zulu(String.upcase(ap)), str_to_int!(min)}
end
# this is a fail if we don't consider timezone
# drop today's time, then add it back in
reference
|> Timex.to_date()
|> Timex.to_datetime()
|> Timex.shift(hours: mhr)
|> Timex.shift(minutes: mmin)
# now_t = Timex.to_unix(reference)
# midnight_t = now_t - rem(now_t, 86400)
# adjusted_t = midnight_t + mhr * 3600 + mmin * 60
# DateTime.from_unix!(adjusted_t)
end
def time_duration([match, match1]), do: time_duration([match, match1, ""])
def time_duration([_match, match1, match2]) do
to_minutes(match1, match2)
end
def time_duration(input) when is_binary(input) do
time_duration(Regex.run(~r/^\s*([0-9.]+)\s*([a-z]+)?/i, input))
end
def time_duration(nil), do: nil
defp ifloor(number) when is_float(number), do: Kernel.trunc(number)
defp ifloor(number) when is_integer(number), do: number
def to_minutes(number, label) do
{:ok, num} = str_to_float(number)
enum_rx(
[
{~r/^(m|min(s)?|minute(s)?)$/, fn _, _ -> num end},
{~r/^(h|hr(s)?|hour(s)?)$/, fn _, _ -> num * 60 end},
{~r/^(d|day(s)?)$/, fn _, _ -> num * 60 * 24 end},
{~r/^(s|sec(s)?|second(s)?)$/, fn _, _ -> num / 60 end},
{~r/now/, fn _, _ -> 0 end},
{~r//, fn _, _ -> num end}
],
label
)
|> ifloor
end
end
#####################################
# the above also references enum_rx() which is from another lib:
@doc """
like Enum.find_value() on a list of [{rx, fn}, ..], calling fn on the matched
rx and returning the result. Almost like a case statement
iex> opts = [
...> {~r/^(\\d+)\\s*(m|min(s)?|minute(s)?)$/, fn match, _ -> {:min, match} end},
...> {~r/^(\\d+)\\s*(h|hour(s)?|hr(s)?)$/, fn match, _ -> {:hr, match} end},
...> ]
...> enum_rx(opts, "30 m")
{:min, ["30 m", "30", "m"]}
iex> enum_rx(opts, "1.5 hr") # doesn't match because of the period
nil
"""
def enum_rx([], _str), do: nil
def enum_rx(elems, str) do
[elem | elems] = elems
{rx, func} = elem
case Regex.run(rx, str) do
nil ->
enum_rx(elems, str)
match ->
func.(match, str)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment