Current section
Files
Jump to
Current section
Files
src/ratioed.gleam
import gleam/int
import gleam/float
import gleam/order.{type Order}
/// Default epsilon for converting floating point numbers
const epsilon = 4.440892098500626e-16
/// Ratio represents a rational number, i.e. one that can be represented as a fraction of two integers
pub opaque type Ratio {
Ratio(n: Int, d: Int)
}
/// Create a ratio from an integer
pub fn from_i(n: Int) -> Ratio {
Ratio(n, 1)
}
/// Create a rational given an integer numerator and denominator
pub fn from_i2(numerator n: Int, denominator d: Int) -> Ratio {
case n, d {
_, 0 | 0, _ -> Ratio(0, 1)
_, _ if d < 0 -> simplify(-n, -d)
_, _ -> simplify(n, d)
}
}
/// Create a rational from a float with maximum error of 2^-51
pub fn from_f(f: Float) -> Ratio {
approx_ratio(epsilon, f)
}
/// Create a rational from a float with a given maximum error
pub fn from_f_epsilon(f: Float, e: Float) -> Ratio {
approx_ratio(e, f)
}
/// Convert a rational to an integer, truncating (round towards 0)
pub fn to_i(a: Ratio) -> Int {
a.n / a.d
}
/// Convert a rational to a floating point number
pub fn to_f(ratio: Ratio) -> Float {
int.to_float(ratio.n) /. int.to_float(ratio.d)
}
// Convert a rational to string
pub fn to_string(ratio: Ratio) -> String {
int.to_string(ratio.n) <> "/" <> int.to_string(ratio.d)
}
/// Compare two rationals
pub fn compare(a: Ratio, b: Ratio) -> Order {
int.compare(a.n * b.d, a.d * b.n)
}
/// Add two rationals
pub fn add(a: Ratio, b: Ratio) -> Ratio {
from_i2(a.n * b.d + a.d * b.n, a.d * b.d)
}
/// Subtract one rational from another
pub fn sub(a: Ratio, b: Ratio) -> Ratio {
from_i2(a.n * b.d - a.d * b.n, a.d * b.d)
}
/// Multiply two rationals
pub fn mul(a: Ratio, b: Ratio) -> Ratio {
from_i2(a.n * b.n, a.d * b.d)
}
/// Divide one rational by another, division by 0 will return 0.
pub fn div(a: Ratio, b: Ratio) -> Ratio {
from_i2(a.n * b.d, a.d * b.n)
}
fn simplify(n: Int, d: Int) -> Ratio {
let gcd = gcd(n, d)
Ratio(n / gcd, d / gcd)
}
fn abs(x: Int) -> Int {
case x < 0 {
True -> -x
False -> x
}
}
fn gcd(x: Int, y: Int) -> Int {
do_gcd(abs(x), abs(y))
}
fn do_gcd(x: Int, y: Int) -> Int {
case x == 0 {
True -> y
False -> {
let assert Ok(z) = int.modulo(y, x)
do_gcd(z, x)
}
}
}
fn approx_ratio(tol: Float, x: Float) -> Ratio {
let a = float.floor(x)
let r = x -. a
continued_fraction(tol, 1, 0, 0, 1, x, 1.0, r, a, tol, 0.0, tol)
}
fn continued_fraction(tol, p, q, pp, qq, x, y, r, a, nt, t, tt) -> Ratio {
case r >. nt {
True -> {
let np = float.truncate(a) * p + pp
let nq = float.truncate(a) * q + qq
let pp = p
let p = np
let qq = q
let q = nq
let x = y
let y = r
let r = fmod(x, y)
let a = float.floor(x /. y)
let tt = t
let t = nt
let nt = a *. t +. tt
continued_fraction(tol, p, q, pp, qq, x, y, r, a, nt, t, tt)
}
False -> {
let i = float.truncate(float.ceiling({ x -. tt } /. { y +. t }))
from_i2(i * p + pp, i * q + qq)
}
}
}
@external(erlang, "math", "fmod")
@external(javascript, "./ratioed.ffi.mjs", "fmod")
fn fmod(_a: Float, _b: Float) -> Float {
0.0
}