defmodule Tox.Time do
@moduledoc """
A set of functions to work with `Time`.
"""
alias Tox.IsoDays
@doc """
Adds `durations` to the given `naive_datetime`.
The `durations` is a keyword list of one or more durations of the type
`Tox.duration` e.g. `[hour: 1, minute: 5, second: 500]`.
## Examples
iex> time = ~T[12:00:00]
iex> Tox.Time.shift(time, hour: 2)
~T[14:00:00.000000]
iex> Tox.Time.shift(time, hour: -2, minute: 10, second: 48)
~T[10:10:48.000000]
iex> Tox.Time.shift(time, day: 2)
~T[12:00:00.000000]
iex> Tox.Time.shift(time, minute: 90)
~T[13:30:00.000000]
iex> Tox.Time.shift(time, minute: -90)
~T[10:30:00.000000]
iex> Tox.Time.shift(time, minute: -59, hour: -23)
~T[12:01:00.000000]
iex> Tox.Time.shift(time, minute: -24 * 60)
~T[12:00:00.000000]
iex> Tox.Time.shift(time, second: 24 * 60 * 60)
~T[12:00:00.000000]
"""
@spec shift(Calendar.time(), [Tox.duration()]) :: Time.t()
def shift(
%{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: {_, precision} = microsecond
},
durations
) do
{parts_in_day, parts_per_day} =
calendar.time_to_day_fraction(hour, minute, second, microsecond)
{_, {parts, _}} = IsoDays.from_durations_time(durations, calendar, precision)
from_day_fraction({parts_in_day + parts, parts_per_day}, calendar)
end
@doc """
Returns true if `time1` occurs after `time2`.
## Examples
iex> Tox.Time.after?(~T[10:00:00], ~T[10:00:00.1])
false
iex> Tox.Time.after?(~T[12:00:00], ~T[11:59:59])
true
iex> Tox.Time.after?(~T[12:00:00], ~T[12:00:00])
false
iex> Tox.Time.after?(
...> Time.convert!(~T[23:23:23], Cldr.Calendar.Coptic),
...> Time.convert!(~T[01:59:59], Cldr.Calendar.Coptic)
...> )
true
"""
defmacro after?(time1, time2) do
quote do
Time.compare(unquote(time1), unquote(time2)) == :gt
end
end
@doc """
Returns true if `time1` occurs after `time2` or both dates are equal.
## Examples
iex> Tox.Time.after_or_equal?(~T[10:00:00], ~T[10:00:00.1])
false
iex> Tox.Time.after_or_equal?(~T[12:00:00], ~T[11:59:59])
true
iex> Tox.Time.after_or_equal?(~T[12:00:00], ~T[12:00:00])
true
iex> Tox.Time.after_or_equal?(
...> Time.convert!(~T[23:23:23], Cldr.Calendar.Coptic),
...> Time.convert!(~T[01:59:59], Cldr.Calendar.Coptic)
...> )
true
"""
defmacro after_or_equal?(time1, time2) do
quote do
Time.compare(unquote(time1), unquote(time2)) in [:gt, :eq]
end
end
@doc """
Returns true if both times are equal.
## Examples
iex> Tox.Time.equal?(~T[11:11:11], ~T[22:22:22])
false
iex> Tox.Time.equal?(~T[12:12:12], ~T[12:12:12])
true
"""
defmacro equal?(time1, time2) do
quote do
Time.compare(unquote(time1), unquote(time2)) == :eq
end
end
@doc """
Returns true if `time1` occurs before `time2`.
## Examples
iex> Tox.Time.before?(~T[10:00:00], ~T[10:00:00.1])
true
iex> Tox.Time.before?(~T[12:00:00], ~T[11:59:59])
false
iex> Tox.Time.before?(~T[12:00:00], ~T[12:00:00])
false
iex> Tox.Time.before?(
...> Time.convert!(~T[23:23:23], Cldr.Calendar.Coptic),
...> Time.convert!(~T[01:59:59], Cldr.Calendar.Coptic)
...> )
false
"""
defmacro before?(time1, time2) do
quote do
Time.compare(unquote(time1), unquote(time2)) == :lt
end
end
@doc """
Returns true if `time1` occurs before `time2` or both dates are equal.
## Examples
iex> Tox.Time.before_or_equal?(~T[10:00:00], ~T[10:00:00.1])
true
iex> Tox.Time.before_or_equal?(~T[12:00:00], ~T[11:59:59])
false
iex> Tox.Time.before_or_equal?(~T[12:00:00], ~T[12:00:00])
true
iex> Tox.Time.before_or_equal?(
...> Time.convert!(~T[23:23:23], Cldr.Calendar.Coptic),
...> Time.convert!(~T[01:59:59], Cldr.Calendar.Coptic)
...> )
false
"""
defmacro before_or_equal?(time1, time2) do
quote do
Time.compare(unquote(time1), unquote(time2)) in [:lt, :eq]
end
end
@doc """
Returns a boolean indicating whether `date` occurs between `from` and `to`.
The optional `boundaries` specifies whether `from` and `to` are included or
not. The possible value for `boundaries` are:
* `:open`: `from` and `to` are excluded
* `:closed`: `from` and `to` are included
* `:left_open`: `from` is excluded and `to` is included
* `:right_open`: `from` is included and `to` is excluded
## Examples
iex> from = ~T[10:00:00]
iex> to = ~T[14:00:00]
iex> Tox.Time.between?(~T[09:00:00], from, to)
false
iex> Tox.Time.between?(~T[12:00:00], from, to)
true
iex> Tox.Time.between?(~T[23:00:00], from, to)
false
iex> Tox.Time.between?(~T[10:00:00], from, to)
true
iex> Tox.Time.between?(~T[14:00:00], from, to)
false
iex> Tox.Time.between?(~T[10:00:00], from, to, :open)
false
iex> Tox.Time.between?(~T[14:00:00], from, to, :open)
false
iex> Tox.Time.between?(~T[10:00:00], from, to, :closed)
true
iex> Tox.Time.between?(~T[14:00:00], from, to, :closed)
true
iex> Tox.Time.between?(~T[10:00:00], from, to, :left_open)
false
iex> Tox.Time.between?(~T[14:00:00], from, to, :left_open)
true
iex> Tox.Time.between?(~T[00:00:00], to, from)
** (ArgumentError) from is equal or greater as to
"""
@spec between?(Calendar.time(), Calendar.time(), Calendar.time(), Tox.boundaries()) ::
boolean()
def between?(time, from, to, boundaries \\ :right_open)
when boundaries in [:closed, :left_open, :right_open, :open] do
if Time.compare(from, to) in [:gt, :eq],
do: raise(ArgumentError, "from is equal or greater as to")
case {Time.compare(time, from), Time.compare(time, to), boundaries} do
{:lt, _, _} -> false
{_, :gt, _} -> false
{:eq, _, :closed} -> true
{:eq, _, :right_open} -> true
{_, :eq, :closed} -> true
{_, :eq, :left_open} -> true
{:gt, :lt, _} -> true
{_, _, _} -> false
end
end
@doc """
Return the minimal time.
## Examples
iex> Tox.Time.min()
~T[00:00:00]
iex> Tox.Time.min(Cldr.Calendar.Coptic)
%Time{
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0},
calendar: Cldr.Calendar.Coptic
}
"""
@spec min(Calendar.calendar()) :: Time.t()
def min(calendar \\ Calendar.ISO) do
{:ok, time} = Time.new(0, 0, 0, {0, 0}, calendar)
time
end
@doc """
Return the maximum time.
## Example
iex> Tox.Time.max()
~T[23:59:59.999999]
iex> Tox.Time.max(Cldr.Calendar.Ethiopic)
%Time{
hour: 23,
minute: 59,
second: 59,
microsecond: {999999, 6},
calendar: Cldr.Calendar.Ethiopic
}
"""
@spec max(Calendar.calendar()) :: Time.t()
def max(calendar \\ Calendar.ISO) do
{hour, minute, second, microsecond} = max_tuple(calendar)
{:ok, time} = Time.new(hour, minute, second, microsecond, calendar)
time
end
@doc false
@spec max_tuple(Calendar.calendar()) ::
{Calendar.hour(), Calendar.minute(), Calendar.second(), Calendar.microsecond()}
def max_tuple(calendar) do
{_, parts_per_day} = calendar.time_to_day_fraction(0, 0, 0, {0, 0})
calendar.time_from_day_fraction({parts_per_day - 1, parts_per_day})
end
# Helper
defp from_day_fraction({parts_in_day, parts_per_day}, calendar) do
remainder = rem(parts_in_day, parts_per_day)
new_parts_in_day =
case remainder < 0 do
true -> parts_per_day + remainder
false -> remainder
end
{hour, minute, second, microsecond} =
calendar.time_from_day_fraction({new_parts_in_day, parts_per_day})
{:ok, time} = Time.new(hour, minute, second, microsecond, calendar)
time
end
end