Last active
October 10, 2019 19:41
-
-
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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