Current section

Files

Jump to
vtc lib utils rational.ex
Raw

lib/utils/rational.ex

defmodule Vtc.Utils.Rational do
@moduledoc false
import Kernel, except: [rem: 2]
@doc """
Rounds `x` based on `method`.
## Arguments
- `x`: The Rational value to round.
- `method`: Rounding strategy. Defaults to `:closest`.
- `:closest`: Round the to the closet whole frame, rounding up when fractional
remainder is equal to `1/2`.
- `:floor`: Always round down to the closest whole-frame.
- `:ciel`: Always round up to the closest whole-frame.
- `:trunc`: Always rounds towards zero.
- `:off`: Pass value through without rounding.
"""
@spec round(Ratio.t(), :closest | :floor | :ceil | :trunc) :: integer()
@spec round(Ratio.t(), :off) :: Ratio.t()
def round(x, method \\ :closest)
def round(%{numerator: n, denominator: d}, :closest), do: round_closest(n, d)
def round(x, :floor), do: Ratio.floor(x)
def round(x, :ceil), do: Ratio.ceil(x)
def round(x, :trunc), do: Ratio.trunc(x)
def round(x, :off), do: x
# Handles rounding to the closest integer. Adapted loosely from Python's
# implementation, only rounds up rather than down:
# https://github.com/python/cpython/blob/3.11/Lib/fractions.py
@spec round_closest(integer(), pos_integer()) :: integer()
defp round_closest(n, d) when n * d < 0, do: -round_closest(-n, d)
defp round_closest(n, d) when Kernel.rem(n, d) * 2 < d, do: div(n, d)
defp round_closest(n, d), do: div(n, d) + 1
@doc """
Does the divrem operation on a rational vale, returns a
{whole_dividend, rational_remainder} tuple.
"""
@spec divrem(Ratio.t(), Ratio.t() | number()) :: {integer(), Ratio.t()}
def divrem(dividend, divisor) do
dividend = Ratio.new(dividend)
divisor = Ratio.new(divisor)
# Round towards zero instead of infinity
quotient = dividend |> Ratio.div(divisor) |> then(&div(&1.numerator, &1.denominator))
remainder = rem(dividend, divisor)
{quotient, remainder}
end
@spec rem(Ratio.t(), Ratio.t()) :: Ratio.t()
def rem(dividend, divisor) do
numerator = Kernel.rem(dividend.numerator * divisor.denominator, divisor.numerator * dividend.denominator)
denominator = dividend.denominator * divisor.denominator
result = Ratio.new(numerator, denominator)
if dividend.numerator < 0 do
%Ratio{numerator: abs(result.numerator) * -1, denominator: result.denominator}
else
result
end
end
end