defmodule Vtc.Utils.Rational do @moduledoc """ Utilities for working with `Ratio` vales. """ @typedoc """ The Ratio module will often convert itself to an integer value if the result would be a whole number, but otherwise return a %Ratio{} struct. This type can be used when working with such a value. """ @type t() :: Ratio.t() | integer() @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. - `:off`: Pass value through without rounding. """ @spec round(t(), :closest | :floor | :ceil) :: integer() @spec round(t(), :off) :: t() def round(x, method \\ :closest) def round(x, _) when is_integer(x), do: x 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, :off), do: x # Handles roundinf to the closest integer. Adapted loosly 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 < 0, do: -round_closest(-n, d) defp round_closest(n, d) when 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(t(), t()) :: {integer(), t()} def divrem(x, divisor) when is_integer(x) and x < 0, do: divrem(%Ratio{numerator: x, denominator: 1}, divisor) def divrem(%{numerator: n} = dividend, divisor) when n < 0, do: dividend |> Ratio.abs() |> divrem(divisor) |> then(fn {q, r} -> {-q, r} end) def divrem(dividend, divisor) do quotient = dividend |> Ratio.div(divisor) |> Ratio.floor() remainder = Ratio.sub(dividend, Ratio.mult(divisor, quotient)) {quotient, remainder} end end