Current section

Files

Jump to
rivet_utils lib time.ex
Raw

lib/time.ex

defmodule Rivet.Utils.Time do
@moduledoc """
Contributor: Brandon Gillespie
"""
import Rivet.Utils.Types, 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