defmodule Rivet.Utils.Time do @moduledoc """ Contributor: Brandon Gillespie """ import Transmogrify.As, only: [as_float: 1] import Rivet.Utils.Enum, only: [enum_rx: 2] def ifloor(number) when is_float(number), do: Kernel.trunc(number) def ifloor(number) when is_integer(number), do: number # note for future @doc """ iex> utc_offset(:string) "-06:00" iex> utc_offset(:minutes) -360 iex> utc_offset(:hour_min) {-6, 0} """ def utc_offset(:string) do p = fn x -> String.pad_leading("#{x}", 2, "0") end case :calendar.time_difference(:calendar.universal_time(), :calendar.local_time()) do {0, {hour, min, _}} -> "+#{p.(hour)}:#{p.(min)}" {-1, {hour, min, _}} -> "-#{p.(24 - hour)}:#{p.(min)}" end end def utc_offset(:minutes) do case :calendar.time_difference(:calendar.universal_time(), :calendar.local_time()) do {0, {hour, min, _}} -> hour * 60 + min {-1, {hour, min, _}} -> (hour - 24) * 60 - min end end def utc_offset(:hour_min) do case :calendar.time_difference(:calendar.universal_time(), :calendar.local_time()) do {0, {hour, min, _}} -> {hour, min} {-1, {hour, min, _}} -> {hour - 24, -min} end end 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, DateTime.to_unix(d_etime) - DateTime.to_unix(d_stime)} else {:error, reason} -> IO.inspect(reason, label: "processing time error") {:error, "Unable to process time: #{reason} #{stime}/#{etime}"} end end #### Disabled because it requires us to include Timex # @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, %DateTime{} = reference) 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] -> # {as_int!(hr, 0), as_int!(min, 0)} # # [_, _, hr, min, _ampm, ap] -> # {as_int!(hr, 0) |> hr_to_zulu(String.upcase(ap)), as_int!(min, 0)} # 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 def to_minutes(number, label) do {:ok, num} = as_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 @doc """ this wraps the complexities brought on by Erlang being fancy with time, and gives us a conventional posix/epoch time value. The time value to return is specified by the first argument, as an atom, and is a required argument (to remove ambiguity), from the set: :second, :millisecond, :microsecond or :nanosecond It also uses their best value for 'monotonic' time so the clock will not go backwards. For the full story see: https://hexdocs.pm/elixir/System.html http://erlang.org/doc/apps/erts/time_correction.html """ @spec epoch_time(time_type :: atom) :: integer def epoch_time(time_type \\ :second) when time_type in [:second, :millisecond, :microsecond, :nanosecond] do System.monotonic_time(time_type) + System.time_offset(time_type) end # and this is just system time to avoid an extra + time_offset operation def now(), do: System.system_time(:second) def now(time_type), do: System.system_time(time_type) ###################################################################################################################### @doc """ start_datetime_of_posix_interval/4. Suppose you have a DateTime that is 30 seconds past the beginning of a given minute. If the interval is 60_000 milliseconds (1 min.), then the beginning of the next posix interval is 30 seconds later- this returns that datetime. """ @spec start_datetime_of_posix_interval( DateTime.t(), integer(), interval_unit :: :second | :millisecond, integer() ) :: DateTime.t() def start_datetime_of_posix_interval( datetime, interval, :second, num_of_intervals_back_or_forward ) do fn posix_time -> div(posix_time, interval) end |> do_find_start(datetime, interval, :second, num_of_intervals_back_or_forward) end @second_to_millisecond_unit_converter 1_000 def start_datetime_of_posix_interval( datetime, interval, :millisecond, num_of_intervals_back_or_forward ) do fn posix_time -> div(posix_time * @second_to_millisecond_unit_converter, interval) end |> do_find_start(datetime, interval, :millisecond, num_of_intervals_back_or_forward) end ###################################################################################################################### @spec do_find_start(fun(), DateTime.t(), integer(), :second | :millisecond, integer()) :: DateTime.t() defp do_find_start( posix_interval_func, datetime, interval, unit, num_of_intervals_back_or_forward ) do datetime |> DateTime.to_unix() |> then(posix_interval_func) |> Kernel.+(num_of_intervals_back_or_forward) |> Kernel.*(interval) |> DateTime.from_unix(unit) |> then(fn {:ok, datetime} -> DateTime.truncate(datetime, :second) end) end @hour_in_seconds 3_600 @midnight ~T[00:00:00] @doc """ utc_hours_since_midnight_sunday/0. There are 168 hours in a week, we zero-index them from 12:00:00 a.m. UTC Sunday. """ @spec utc_hours_since_midnight_sunday() :: 0..167 def utc_hours_since_midnight_sunday() do utc_now = DateTime.utc_now() utc_now |> Date.beginning_of_week() # beginning of week is Monday, subtract one day for Sunday |> Date.add(-1) |> DateTime.new(@midnight) |> then(fn {:ok, sunday_midnight} -> DateTime.to_unix(sunday_midnight) end) |> then(fn unix_sunday_midnight -> div(DateTime.to_unix(utc_now) - unix_sunday_midnight, @hour_in_seconds) end) end end