## lib/ratio.ex

``````defmodule Ratio do
@vsn "3.0.0"

import Kernel,
except: [
div: 2,
abs: 1,
floor: 1,
ceil: 1,
trunc: 1
]

@moduledoc """
This module allows you to use Rational numbers in Elixir, to enable exact calculations with all numbers big and small.

`Ratio` defines arithmetic and comparison operations to work with rational numbers.

Usually, you probably want to add the line `import Ratio, only: [<|>: 2]` to your code.

## Shorthand operator

Rational numbers can be written using the operator `<|>` (as in: `1 <|> 2`), which is also how Ratio structs are pretty-printed when inspecting.
`a <|> b` is a shorthand for `Ratio.new(a, b)`.

## Inline Math Operators and Casting

Ratio interopts with the `Numbers` library:
If you want to overload Elixir's builtin math operators, you can use `use Numbers, overload_operators: true`.

This also allows you to pass in a rational number as one argument
and an integer, float or Decimal (if you have installed the `Decimal` library),
which are then cast to rational numbers whenever necessary.

"""

defmacro __using__(_opts) do
raise """
Writing `use Ratio` (with or without options) is no longer possible in version 3.

- To only use the rational number creation shorthand operator, write `import Ratio, only: [<|>: 2]`
- To override the inline math operators, write `use Numbers, overload_operators: true`. (and see the `Numbers` module/library for more information.)
"""
end

@doc """
A Rational number is defined as a numerator and a denominator.
Both the numerator and the denominator are integers.
If you want to match for a rational number, you can do so by matching against this Struct.

Note that *directly manipulating* the struct, however, is usually a bad idea, as then there are no validity checks, nor wil the rational be simplified.

"""
defstruct numerator: 0, denominator: 1
@type t :: %Ratio{numerator: integer(), denominator: pos_integer()}

@doc """
Check to see whether something is a ratioal struct.

On recent OTP versions that expose `:erlang.map_get/2` this function is guard safe.

iex> require Ratio
iex> Ratio.is_rational(1 <|> 2)
true
iex> Ratio.is_rational(Ratio.new(10))
true
iex> Ratio.is_rational(42)
false
iex> Ratio.is_rational(%{})
false
iex> Ratio.is_rational("My quick brown fox")
false
"""
if function_exported?(:erlang, :map_get, 2) and function_exported?(Kernel, :is_map_key, 2) do
defguard is_rational(val)
when is_map(val) and is_map_key(val, :__struct__) and is_struct(val) and
:erlang.map_get(:__struct__, val) == __MODULE__
else
def is_rational(val)
def is_rational(%Ratio{}), do: true
def is_rational(_), do: false
end

@doc """
Creates a new Rational number.
This number is simplified to the most basic form automatically.

Rational numbers with a `0` as denominator are not allowed.

Note that it is recommended to use integer numbers for the numerator and the denominator.

## Floats

*If possible, don't use them.*

Using Floats for the numerator or denominator is possible, however, because base-2 floats cannot represent all base-10 fractions properly, the results might be different from what you might expect.

Floats are converted into rationals by using `Float.ratio` (since version 3.0).

## Decimals

To use `Decimal` parameters, the [decimal](https://hex.pm/packages/decimal) library must
be configured in `mix.exs`.

## Examples

iex> 1 <|> 2
1 <|> 2
iex> 100 <|> 300
1 <|> 3
iex> 1.5 <|> 4
3 <|> 8
iex> (3 <|> 2) <|> 3
1 <|> 2
iex> 3 <|> (3<|>2)
2 <|> 1
iex> (3<|>2) <|> (1<|>3)
9 <|> 2
"""
def numerator <|> denominator

def _numerator <|> 0 do
raise ArithmeticError
end

def numerator <|> denominator when is_integer(numerator) and is_integer(denominator) do
simplify(%Ratio{numerator: numerator, denominator: denominator})
end

def numerator <|> denominator when is_float(numerator) do
div(Ratio.FloatConversion.float_to_rational(numerator), Ratio.new(denominator))
end

def numerator <|> denominator when is_float(denominator) do
div(numerator, Ratio.FloatConversion.float_to_rational(denominator))
end

def (numerator = %Ratio{}) <|> (denominator = %Ratio{}) do
div(numerator, denominator)
end

def (numerator = %Decimal{}) <|> (denominator = %Decimal{}) do
Ratio.DecimalConversion.decimal_to_rational(numerator)
|> div(Ratio.DecimalConversion.decimal_to_rational(denominator))
end

def (numerator = %Decimal{}) <|> denominator when is_float(denominator) do
Ratio.DecimalConversion.decimal_to_rational(numerator)
|> div(Ratio.FloatConversion.float_to_rational(denominator))
end

def numerator <|> (denominator = %Decimal{}) when is_float(numerator) do
Ratio.FloatConversion.float_to_rational(numerator)
|> div(Ratio.DecimalConversion.decimal_to_rational(denominator))
end

def (numerator = %Decimal{}) <|> denominator when is_integer(denominator) do
Ratio.DecimalConversion.decimal_to_rational(numerator)
|> div(denominator)
end

def numerator <|> (denominator = %Decimal{}) when is_integer(numerator) do
div(Ratio.DecimalConversion.decimal_to_rational(numerator), denominator)
end
end

def numerator <|> (denominator = %Ratio{}) when is_integer(numerator) do
div(%Ratio{numerator: numerator, denominator: 1}, denominator)
end

def (numerator = %Ratio{}) <|> denominator when is_integer(denominator) do
div(numerator, %Ratio{numerator: denominator, denominator: 1})
end

@doc """
Prefix-version of `numerator <|> denominator`.
Useful when `<|>` is not available (for instance, when already in use by another module)

Not imported when calling `use Ratio`, so always call it as `Ratio.new(a, b)`

To use `Decimal` parameters, the [decimal](https://hex.pm/packages/decimal) library must
be configured in `mix.exs`.

## Examples

iex> Ratio.new(1, 2)
1 <|> 2
iex> Ratio.new(100, 300)
1 <|> 3

"""
def new(numerator, denominator \\ 1)

def new(%Decimal{} = decimal, 1) do
Ratio.DecimalConversion.decimal_to_rational(decimal)
end

def new(%Decimal{} = numerator, %Decimal{} = denominator) do
Ratio.DecimalConversion.decimal_to_rational(numerator)
<|> Ratio.DecimalConversion.decimal_to_rational(denominator)
end

def new(numerator, %Decimal{} = denominator) do
numerator <|> Ratio.DecimalConversion.decimal_to_rational(denominator)
end
end

def new(numerator, denominator) do
numerator <|> denominator
end

@doc """
Returns the absolute version of the given number (which might be an integer, float or Rational).

## Examples

iex>Ratio.abs(-5 <|> 2)
5 <|> 2
"""
def abs(number) when is_number(number), do: Kernel.abs(number)

def abs(%Ratio{numerator: numerator, denominator: denominator}),
do: Kernel.abs(numerator) <|> denominator

@doc """
Returns the sign of the given number (which might be an integer, float or Rational)

This is:

- 1 if the number is positive.
- -1 if the number is negative.
- 0 if the number is zero.

"""
def sign(%Ratio{numerator: numerator}) when Kernel.>(numerator, 0), do: 1
def sign(%Ratio{numerator: numerator}) when Kernel.<(numerator, 0), do: Kernel.-(1)
def sign(number) when is_number(number) and Kernel.>(number, 0), do: 1
def sign(number) when is_number(number) and Kernel.<(number, 0), do: Kernel.-(1)
def sign(number) when is_number(number), do: 0

@doc """
Converts the passed *number* as a Rational number, and extracts its denominator.
For integers returns the passed number itself.

"""
def numerator(number) when is_integer(number), do: number

def numerator(number) when is_float(number),
do: numerator(Ratio.FloatConversion.float_to_rational(number))

def numerator(%Ratio{numerator: numerator}), do: numerator

@doc """
Treats the passed *number* as a Rational number, and extracts its denominator.
For integers, returns `1`.
"""
def denominator(number) when is_number(number), do: 1
def denominator(%Ratio{denominator: denominator}), do: denominator

@doc """
"""

def add(%Ratio{numerator: a, denominator: lcm}, %Ratio{numerator: c, denominator: lcm}) do
Kernel.+(a, c) <|> lcm
end

def add(%Ratio{numerator: a, denominator: b}, %Ratio{numerator: c, denominator: d}) do
Kernel.+(a * d, c * b) <|> (b * d)
end

@doc """
Subtracts the rational number *b* from the rational number *a*.
"""
def sub(a, b), do: add(a, minus(b))

@doc """
Negates the given rational number.

## Examples

iex> Ratio.minus(5 <|> 3)
-5 <|> 3
"""
def minus(%Ratio{numerator: numerator, denominator: denominator}) do
%Ratio{numerator: Kernel.-(numerator), denominator: denominator}
end

@doc """
Multiplies two rational numbers.

# Examples

iex> Ratio.mult( 1 <|> 3, 1 <|> 2)
1 <|> 6
"""
def mult(number1, number2)

def mult(%Ratio{numerator: numerator1, denominator: denominator1}, %Ratio{
numerator: numerator2,
denominator: denominator2
}) do
Kernel.*(numerator1, numerator2) <|> Kernel.*(denominator1, denominator2)
end

@doc """
Divides the rational number *a* by the rational number *b*.

## Examples

iex> Ratio.div(2 <|> 3, 8 <|> 5)
5 <|> 12

"""
def div(a, b)

def div(%Ratio{numerator: numerator1, denominator: denominator1}, %Ratio{
numerator: numerator2,
denominator: denominator2
}) do
Kernel.*(numerator1, denominator2) <|> Kernel.*(denominator1, numerator2)
end

defmodule ComparisonError do
defexception message: "These things cannot be compared."
end

@doc """
Compares two rational numbers, returning `:lt`, `:eg` or `:gt`
depending on whether *a* is less than, equal to or greater than *b*, respectively.

This function is able to compare rational numbers against integers or floats as well.

This function accepts other types as input as well, comparing them using Erlang's Term Ordering.
This is mostly useful if you have a collection that contains other kinds of numbers (builtin integers or floats) as well.

"""
# TODO enhance this function to work with other number types?
def compare(%Ratio{numerator: a, denominator: b}, %Ratio{numerator: c, denominator: d}) do
compare(Kernel.*(a, d), Kernel.*(b, c))
end

def compare(%Ratio{numerator: numerator, denominator: denominator}, b) do
compare(numerator, Kernel.*(b, denominator))
end

def compare(a, %Ratio{numerator: numerator, denominator: denominator}) do
compare(Kernel.*(a, denominator), numerator)
end

# Fallback using the builting Erlang term ordering.
def compare(a, b) do
case {a, b} do
{a, b} when a > b -> :gt
{a, b} when a < b -> :lt
_ -> :eq
end
end

@doc """
True if *a* is equal to *b*
"""
def eq?(a, b), do: compare(a, b) |> Kernel.==(:eq)

@doc """
True if *a* is larger than or equal to *b*
"""
def gt?(a, b), do: compare(a, b) |> Kernel.==(:gt)

@doc """
True if *a* is smaller than *b*
"""
def lt?(a, b), do: compare(a, b) |> Kernel.==(:lt)

@doc """
True if *a* is larger than or equal to *b*
"""
def gte?(a, b), do: compare(a, b) in [:eq, :gt]

@doc """
True if *a* is smaller than or equal to *b*
"""
def lte?(a, b), do: compare(a, b) in [:lt, :eq]

@doc """
True if *a* is equal to *b*?
"""
def equal?(a, b), do: compare(a, b) |> Kernel.==(:eq)

@doc """
returns *x* to the *n* th power.

*x* is allowed to be an integer, rational or float (in the last case, this is first converted to a rational).

Will give the answer as a rational number when applicable.
Note that the exponent *n* is only allowed to be an integer.

(so it is not possible to compute roots using this function.)

## Examples

iex> Ratio.pow(Ratio.new(2), 4)
16 <|> 1
iex> Ratio.pow(Ratio.new(2), -4)
1 <|> 16
iex> Ratio.pow(3 <|> 2, 10)
59049 <|> 1024
iex> Ratio.pow(Ratio.new(10), 0)
1 <|> 1
"""
@spec pow(number() | Ratio.t(), pos_integer()) :: Ratio.t()
def pow(x, n)

# Convert Float to Rational.
# def pow(x, n) when is_float(x), do: pow(Ratio.FloatConversion.float_to_rational(x), n)

# Small powers
def pow(%__MODULE__{}, 0), do: Ratio.new(1)
def pow(x = %__MODULE__{}, 1), do: x
def pow(x = %__MODULE__{}, 2), do: Ratio.mult(x, x)
def pow(x = %__MODULE__{}, 3), do: Ratio.mult(Ratio.mult(x, x), x)
def pow(x = %__MODULE__{}, n) when is_integer(n), do: do_pow(x, n)

# Exponentiation By Squaring.
defp do_pow(x, n, y \\ 1)
defp do_pow(_x, 0, y), do: y
defp do_pow(x, 1, y), do: Numbers.mult(x, y)
defp do_pow(x, n, y) when Kernel.<(n, 0), do: do_pow(1 <|> x, Kernel.-(n), y)

defp do_pow(x, n, y) when rem(n, 2) |> Kernel.==(0) do
do_pow(Ratio.mult(x, x), Kernel.div(n, 2), y)
end

defp do_pow(x, n, y) do
do_pow(Ratio.mult(x, x), Kernel.div(n - 1, 2), Numbers.mult(x, y))
end

@doc """
Converts the given *number* to a Float. As floats do not have arbitrary precision, this operation is generally not reversible.

Not imported when calling `use Ratio`, so always call it as `Rational.to_float(number)`
"""
@spec to_float(Ratio.t() | number) :: float
def to_float(%Ratio{numerator: numerator, denominator: denominator}),
do: Kernel./(numerator, denominator)

def to_float(number), do: :erlang.float(number)

@doc """
Returns a tuple, where the first element is the result of `to_float(number)` and
the second is a conversion error.

The conversion error is calculated by subtracting the original number from the
conversion result.

## Examples

iex> Ratio.to_float_error(Ratio.new(1, 2))
{0.5, 0 <|> 1}
iex> Ratio.to_float_error(Ratio.new(2, 3))
{0.6666666666666666, -1 <|> 27021597764222976}
"""
@spec to_float_error(t | number) :: {float, error} when error: t | number
def to_float_error(number) do
float = to_float(number)
error = Ratio.sub(Ratio.new(float), number)
{float, error}
end

@doc """
Returns a binstring representation of the Rational number.
If the denominator is `1` it will still be printed in the `a <|> 1` format.

## Examples

iex> Ratio.to_string 10 <|> 7
"10 <|> 7"
iex> Ratio.to_string 10 <|> 2
"5 <|> 1"
"""
def to_string(rational)

def to_string(%Ratio{numerator: numerator, denominator: denominator}) do
"#{numerator} <|> #{denominator}"
end

defimpl String.Chars, for: Ratio do
def to_string(rational) do
Ratio.to_string(rational)
end
end

defimpl Inspect, for: Ratio do
def inspect(rational, _) do
Ratio.to_string(rational)
end
end

# Simplifies the Rational to its most basic form.
# Which might result in an integer.
# Ensures that a `-` is only kept in the numerator.
defp simplify(rational)

defp simplify(%Ratio{numerator: numerator, denominator: denominator}) do
gcdiv = gcd(numerator, denominator)
new_denominator = Kernel.div(denominator, gcdiv)
{new_denominator, numerator} = normalize_denom_num(new_denominator, numerator)

# if new_denominator == 1 do
#   Kernel.div(numerator, gcdiv)
# else
%Ratio{numerator: Kernel.div(numerator, gcdiv), denominator: new_denominator}
# end
end

defp normalize_denom_num(denominator, numerator) do
if denominator < 0 do
{Kernel.-(denominator), Kernel.-(numerator)}
else
{denominator, numerator}
end
end

# Calculates the Greatest Common denominator of two numbers.
defp gcd(a, 0), do: abs(a)

defp gcd(0, b), do: abs(b)
defp gcd(a, b), do: gcd(b, Kernel.rem(a, b))

@doc """
Rounds a number (rational, integer or float) to the largest whole number less than or equal to num.
For negative numbers, this means we are rounding towards negative infinity.

iex> Ratio.floor(Ratio.new(1, 2))
0
iex> Ratio.floor(Ratio.new(5, 4))
1
iex> Ratio.floor(Ratio.new(-3, 2))
-2

"""
def floor(num) when is_integer(num), do: num
def floor(num) when is_float(num), do: Float.floor(num)

def floor(%Ratio{numerator: numerator, denominator: denominator}),
do: Integer.floor_div(numerator, denominator)

@doc """
Rounds a number (rational, integer or float) to the largest whole number larger than or equal to num.
For negative numbers, this means we are rounding towards negative infinity.

iex> Ratio.ceil(Ratio.new(1, 2))
1
iex> Ratio.ceil(Ratio.new(5, 4))
2
iex> Ratio.ceil(Ratio.new(-3, 2))
-1
iex> Ratio.ceil(Ratio.new(400))
400

"""
def ceil(num) when is_float(num), do: Float.ceil(num)
def ceil(num) when is_integer(num), do: num

def ceil(num = %Ratio{numerator: numerator, denominator: denominator}) do
floor = Ratio.floor(num)

if rem(numerator, denominator) == 0 do
floor
else
floor + 1
end
end

@doc """
Returns the integer part of number.

## Examples

iex> Ratio.trunc(1.7)
1
iex> Ratio.trunc(-1.7)
-1
iex> Ratio.trunc(3)
3
iex> Ratio.trunc(Ratio.new(5, 2))
2
"""
@spec trunc(t | number) :: integer
def trunc(num) when is_integer(num), do: num
def trunc(num) when is_float(num), do: Kernel.trunc(num)

def trunc(%Ratio{numerator: numerator, denominator: denominator}) do
Kernel.div(numerator, denominator)
end
end
``````