Packages

Helper for converting and performing math operations on values of Time, Date, DateTime, and NaiveDateTime along with storing in database with Ecto.Type

Current section

Files

Jump to
periods lib periods computation.ex
Raw

lib/periods/computation.ex

defmodule Periods.Computation do
@moduledoc false
import Kernel, except: [abs: 1]
import Periods.Conversion
import Periods.Parser, only: [parse_unit: 1]
alias Periods.Period
@month_restrictions [:millisecond, :second, :minute, :hour, :week]
@units Periods.all_units()
@non_month_units List.delete(@units, :month)
defmodule ComputationError do
use Periods.Errors
def exception({:invalid_month_addition, unit}) do
%ComputationError{message: "cannot add #{unit} to month"}
end
def exception({:invalid_time_addition, unit}) do
%ComputationError{message: "cannot add #{unit} to Time"}
end
def exception({:invalid_month_subtraction, unit}) do
%ComputationError{message: "cannot subtract #{unit} with a month"}
end
def exception({:invalid_time_subtraction, unit}) do
%ComputationError{message: "cannot subtract #{unit} with Time"}
end
end
def abs(%Period{} = period) do
%Period{amount: Kernel.abs(period.amount), unit: period.unit}
end
def add(%Period{unit: :month}, %Period{unit: unit}) when unit in @month_restrictions do
{:error, {:invalid_month_addition, unit}}
end
def add(%Period{unit: unit}, %Period{unit: :month}) when unit in @month_restrictions do
{:error, {:invalid_month_addition, unit}}
end
def add(%Period{unit: unit} = period_1, %Period{unit: unit} = period_2) do
total = period_1.amount + period_2.amount
%Period{amount: total, unit: unit}
end
def add(%Period{} = period_1, %Period{} = period_2) do
unit = lowest_unit(period_1.unit, period_2.unit)
add(convert(period_1, unit), convert(period_2, unit))
end
def add(%Time{}, %Period{unit: unit}) when unit in [:day, :week, :month, :year, :decade] do
{:error, {:invalid_time_addition, unit}}
end
def add(%Time{} = time, period) do
case convert(period, :second) do
%Period{unit: :second} = converted -> Time.add(time, converted.amount, :second)
{:error, message} -> {:error, message}
end
end
def add(%Date{}, %Period{unit: :month}) do
{:error, {:invalid_month_addition, Date}}
end
def add(%Date{} = date, period) do
case convert(period, :day) do
%Period{unit: :day} = converted -> Date.add(date, converted.amount)
{:error, message} -> {:error, message}
end
end
def add(%DateTime{}, %Period{unit: :month}) do
{:error, {:invalid_month_addition, DateTime}}
end
def add(%DateTime{} = date_time, %Period{} = period) do
case convert(period, :second) do
%Period{unit: :second} = converted -> DateTime.add(date_time, converted.amount, :second)
{:error, message} -> {:error, message}
end
end
def add(%NaiveDateTime{}, %Period{unit: :month}) do
{:error, {:invalid_month_addition, NaiveDateTime}}
end
def add(%NaiveDateTime{} = date_time, %Period{unit: unit} = period)
when unit not in @month_restrictions do
case convert(period, :second) do
%Period{unit: :second} = converted ->
NaiveDateTime.add(date_time, converted.amount, :second)
{:error, message} ->
{:error, message}
end
end
def add(%NaiveDateTime{}, %Period{}) do
{:error, {:invalid_month_addition, NaiveDateTime}}
end
def add(%Period{} = period, other), do: add(other, period)
def diff(computation_type_1, computation_type_2, unit \\ nil)
def diff(%Time{} = time_1, %Time{} = time_2, nil) do
time_1
|> Time.diff(time_2)
|> Periods.new(:second)
end
def diff(%Time{} = time_1, %Time{} = time_2, unit) when unit in [:millisecond, :second] do
time_1
|> Time.diff(time_2, unit)
|> Periods.new(unit)
end
def diff(%Time{}, %Time{}, _unit), do: {:error, :invalid_unit_type}
def diff(%Date{} = date_1, %Date{} = date_2, nil) do
date_1
|> Date.diff(date_2)
|> Periods.new(:day)
end
def diff(%Date{} = date_1, %Date{} = date_2, unit) when unit in @non_month_units do
case diff(date_1, date_2) do
{:ok, period} -> {:ok, convert(period, unit)}
{:error, message} -> {:error, message}
end
end
def diff(%Date{month: month_1, year: year_1}, %Date{month: month_2, year: year_2}, :month) do
month_diff({year_1, month_1}, {year_2, month_2})
end
def diff(
%DateTime{time_zone: time_zone} = datetime_1,
%DateTime{time_zone: time_zone} = datetime_2,
nil
) do
datetime_1
|> DateTime.diff(datetime_2)
|> Periods.new(:second)
end
def diff(
%DateTime{time_zone: time_zone} = datetime_1,
%DateTime{time_zone: time_zone} = datetime_2,
:millisecond
) do
datetime_1
|> DateTime.diff(datetime_2, :millisecond)
|> Periods.new(:millisecond)
end
def diff(
%DateTime{month: month_1, year: year_1, time_zone: time_zone},
%DateTime{month: month_2, year: year_2, time_zone: time_zone},
:month
) do
month_diff({year_1, month_1}, {year_2, month_2})
end
def diff(
%DateTime{time_zone: time_zone} = datetime_1,
%DateTime{time_zone: time_zone} = datetime_2,
unit
)
when unit in @non_month_units do
case diff(datetime_1, datetime_2) do
{:ok, period} -> {:ok, convert(period, unit)}
{:error, message} -> {:error, message}
end
end
def diff(
%DateTime{time_zone: _time_zone_1} = datetime_1,
%DateTime{time_zone: _time_zone_2} = datetime_2,
unit
)
when unit in @units or unit == nil do
{:ok, utc_datetime_1} = DateTime.shift_zone(datetime_1, "Etc/UTC")
{:ok, utc_datetime_2} = DateTime.shift_zone(datetime_2, "Etc/UTC")
diff(utc_datetime_1, utc_datetime_2, unit)
end
def diff(%NaiveDateTime{} = naive_datetime_1, %NaiveDateTime{} = naive_datetime_2, nil) do
naive_datetime_1
|> NaiveDateTime.diff(naive_datetime_2)
|> Periods.new(:second)
end
def diff(%NaiveDateTime{} = naive_datetime_1, %NaiveDateTime{} = naive_datetime_2, :millisecond) do
naive_datetime_1
|> NaiveDateTime.diff(naive_datetime_2, :millisecond)
|> Periods.new(:millisecond)
end
def diff(
%NaiveDateTime{month: month_1, year: year_1},
%NaiveDateTime{month: month_2, year: year_2},
:month
) do
month_diff({year_1, month_1}, {year_2, month_2})
end
def diff(%NaiveDateTime{} = naive_datetime_1, %NaiveDateTime{} = naive_datetime_2, unit)
when unit in @non_month_units do
case diff(naive_datetime_1, naive_datetime_2) do
{:ok, period} -> {:ok, convert(period, unit)}
{:error, message} -> {:error, message}
end
end
def diff(computation_type_1, computation_type_2, unit) when is_binary(unit) do
case parse_unit(unit) do
{:ok, unit} -> diff(computation_type_1, computation_type_2, unit)
{:error, message} -> {:error, message}
end
end
def diff(_computation_type_1, _computation_type_2, unit) when unit in @units,
do: {:error, :invalid_arguments}
def diff(_computation_type_1, _computation_type_2, _unit), do: {:error, :invalid_unit_type}
def multiply(%Period{} = period, scalar) when is_integer(scalar) do
%Period{amount: period.amount * scalar, unit: period.unit}
end
def negate(%Period{} = period) do
%Period{amount: -period.amount, unit: period.unit}
end
def subtract(%Period{unit: :month}, %Period{unit: unit}) when unit in @month_restrictions do
{:error, {:invalid_month_subtraction, unit}}
end
def subtract(%Period{unit: unit}, %Period{unit: :month}) when unit in @month_restrictions do
{:error, {:invalid_month_subtraction, unit}}
end
def subtract(%Period{unit: unit} = period_1, %Period{unit: unit} = period_2) do
total = period_1.amount - period_2.amount
%Period{amount: total, unit: unit}
end
def subtract(%Period{} = period_1, %Period{} = period_2) do
unit = lowest_unit(period_1.unit, period_2.unit)
subtract(convert(period_1, unit), convert(period_2, unit))
end
def subtract(%Time{}, %Period{unit: unit}) when unit in [:day, :week, :month, :year, :decade] do
{:error, {:invalid_time_subtraction, unit}}
end
def subtract(%Time{} = time, period) do
case convert(period, :second) do
%Period{unit: :second} = converted -> Time.add(time, -converted.amount)
{:error, message} -> {:error, message}
end
end
def subtract(%Date{}, %Period{unit: :month}) do
{:error, {:invalid_month_subtraction, Date}}
end
def subtract(%Date{} = date, period) do
case convert(period, :day) do
%Period{unit: :day} = converted -> Date.add(date, -converted.amount)
{:error, message} -> {:error, message}
end
end
def subtract(%DateTime{}, %Period{unit: :month}) do
{:error, {:invalid_month_subtraction, DateTime}}
end
def subtract(%DateTime{} = date_time, %Period{} = period) do
case convert(period, :second) do
%Period{unit: :second} = converted -> DateTime.add(date_time, -converted.amount, :second)
{:error, message} -> {:error, message}
end
end
def subtract(%NaiveDateTime{}, %Period{unit: :month}) do
{:error, {:invalid_month_subtraction, NaiveDateTime}}
end
def subtract(%NaiveDateTime{} = date_time, %Period{unit: unit} = period)
when unit not in @month_restrictions do
case convert(period, :second) do
%Period{unit: :second} = converted ->
NaiveDateTime.add(date_time, -converted.amount, :second)
{:error, message} ->
{:error, message}
end
end
def subtract(%Period{} = period, other), do: subtract(other, period)
defp month_diff({year_1, month_1}, {year_2, month_2}) do
with {:ok, years} <- Periods.new(year_1 - year_2, :year),
{:ok, months} <- Periods.new(month_1 - month_2, :month),
%Period{} = period <- Periods.add(years, months) do
{:ok, period}
end
end
end