Current section
Files
Jump to
Current section
Files
src/glat.gleam
import glat/util
import gleam/float
import gleam/int
import gleam/list
import gleam/order
import gleam/result
import gleam/string
pub type Rational {
/// âš âš It is unsafe to raw construct this from unknown input,
/// as these functions assume the rational they take in do not have a 0 or negative denominator.
/// You should use `new`.
Rational(num: Int, den: Int)
}
/// 0/1
pub const zero = Rational(0, 1)
/// 1/1
pub const one = Rational(1, 1)
/// Returns Error if the denominator is 0.
pub fn new_check(num: Int, over den: Int) -> Result(Rational, Nil) {
case den {
0 -> Error(Nil)
_ -> Ok(Rational(num:, den:) |> shift_negative)
}
}
/// Returns [`zero`](#zero) if the denominator is 0.
pub fn new(num: Int, over den: Int) -> Rational {
new_check(num, den) |> result.unwrap(zero)
}
/// `num` / 1.
pub fn from_int(num: Int) -> Rational {
Rational(num:, den: 1)
}
/// This returns (0..=den)/den. \
/// A negative denominator will negate the result.
///
/// Returns Error if the integer is 0.
pub fn random_check(over den: Int) -> Result(Rational, Nil) {
new_check(int.random(int.absolute_value(den) + 1), den)
}
/// This returns (0..=den)/den. \
/// A negative denominator will negate the result.
///
/// Returns [`zero`](#zero) if den is 0.
pub fn random(over den: Int) -> Rational {
unwrap(random_check(den))
}
/// Returns Error if the float is 0.0.
pub fn from_float_check(f: Float) -> Result(Rational, Nil) {
case f {
0.0 -> Error(Nil)
_ -> {
let assert [whole_str, fract_str] =
float.to_string(f) |> string.split(".")
let fract_str = case fract_str {
"0" -> ""
x -> x
}
let power = string.length(fract_str)
let assert Ok(num) = int.parse(whole_str <> fract_str)
let den = util.int_power(10, power)
Ok(Rational(num, den))
}
}
}
/// Returns [`zero`](#zero) if the float is 0.0.
pub fn from_float(f: Float) -> Rational {
from_float_check(f) |> result.unwrap(zero)
}
/// Only parses `{num}/{den}`.
///
/// Returns Error if the parsing fails, or the denominator is 0.
pub fn parse_check(str: String) -> Result(Rational, Nil) {
use #(num, den) <- result.try(do_parse(str))
new_check(num, den)
}
/// Only parses `{num}/{den}`.
///
/// Returns Error if the parsing fails. \
/// Returns [`zero`](#zero) if the denominator is 0.
pub fn parse(str: String) -> Result(Rational, Nil) {
use #(num, den) <- result.map(do_parse(str))
new(num, den)
}
fn do_parse(str: String) -> Result(#(Int, Int), Nil) {
case str |> string.split("/") {
[num, den] -> {
use num <- result.try(int.parse(num))
use den <- result.try(int.parse(den))
Ok(#(num, den))
}
_ -> Error(Nil)
}
}
/// Result of num / den.
/// Also getting the whole part of a mixed fraction.
pub fn to_int(rat: Rational) -> Int {
rat.num / rat.den
}
/// Result of int.floor_divide(num, den).
/// Also getting the whole part of a mixed fraction.
pub fn to_int_floor(rat: Rational) -> Int {
rat.num |> int.floor_divide(rat.den) |> result.unwrap(0)
}
/// Panics if the numerator or denominator are not in range of a Float.
pub fn to_float(rat: Rational) -> Float {
int.to_float(rat.num) /. int.to_float(rat.den)
}
/// Join the numerator and denominator with a "/".
pub fn to_string(rat: Rational) -> String {
int.to_string(rat.num) <> "/" <> int.to_string(rat.den)
}
pub fn is_negative(rat: Rational) -> Bool {
rat.num < 0
}
pub fn is_positive(rat: Rational) -> Bool {
rat.num >= 0
}
/// Simplify the rational.
pub fn reduce(rat: Rational) -> Rational {
let gcd = util.gcd(rat.num, rat.den)
new(rat.num / gcd, rat.den / gcd)
}
/// |num| / den
pub fn absolute_value(rat: Rational) -> Rational {
Rational(..rat, num: int.absolute_value(rat.num))
}
/// Swap the numerator and denominator.
///
/// Returns an Error if the numerator is 0.
pub fn flip_check(rat: Rational) -> Result(Rational, Nil) {
new_check(rat.den, rat.num)
}
/// Swap the numerator and denominator.
///
/// Returns [`zero`](#zero) if the numerator is 0.
pub fn flip(rat: Rational) -> Rational {
flip_check(rat) |> result.unwrap(zero)
}
/// TODO: make this function (or a new function) not have a float limitation.
///
/// Panics if the numerator and denominator are not in the bounds of a float. \
/// Returns Error if the rational is negative.
pub fn square_root_check(rat: Rational) -> Result(Rational, Nil) {
case rat.num {
0 -> Ok(rat)
_ -> {
use num <- result.try(int.square_root(rat.num))
use den <- result.try(int.square_root(rat.den))
Ok(divide(from_float(num), from_float(den)))
}
}
}
/// TODO: make this function (or a new function) not have a float limitation.
///
/// Panics if the numerator and denominator are not in the bounds of a float. \
/// Returns [`zero`](#zero) if the rational is negative.
pub fn square_root(rat: Rational) -> Rational {
square_root_check(rat) |> result.unwrap(zero)
}
/// This uses truncated division like the `%` operator does.
/// See [`modulo`](#modulo) for floor division.
pub fn remainder(rat: Rational) -> Int {
let assert Ok(num) = int.remainder(rat.num, rat.den)
num
}
/// This uses floor division.
/// See [`remainder`](#remainder) for truncated division.
pub fn modulo(rat: Rational) -> Int {
let assert Ok(num) = int.modulo(rat.num, rat.den)
num
}
/// Get the fractional part of a mixed fraction.
/// Uses truncated division.
pub fn fraction(rat: Rational) -> Rational {
Rational(remainder(rat), rat.den)
}
/// Get the fractional part of a mixed fraction.
/// Uses floored division.
pub fn fraction_floor(rat: Rational) -> Rational {
Rational(modulo(rat), rat.den)
}
/// Rounds the rational upwards.
pub fn floor(rat: Rational) -> Rational {
case remainder(rat) {
0 -> rat
rem ->
case is_positive(rat) {
True -> Rational(rat.num - rem, rat.den)
False -> {
Rational(rat.num - rem - rat.den, rat.den)
}
}
}
}
/// Rounds the rational upwards.
pub fn ceiling(rat: Rational) -> Rational {
case remainder(rat) {
0 -> rat
rem ->
case is_positive(rat) {
True -> Rational(rat.num - rem + rat.den, rat.den)
False -> Rational(rat.num - rem, rat.den)
}
}
}
/// [`floor`](#floor) if it is less than 1/2, [`ceiling`](#ceiling) otherwise.
pub fn round(rat: Rational) -> Rational {
rat
|> case compare(rat, Rational(1, 2)) {
order.Gt | order.Eq -> ceiling
order.Lt -> floor
}
}
pub fn negate(rat: Rational) -> Rational {
Rational(-rat.num, rat.den)
}
pub fn compare(lhs: Rational, rhs: Rational) -> order.Order {
int.compare(lhs.num * rhs.den, lhs.den * rhs.num)
}
pub fn add(lhs: Rational, rhs: Rational) -> Rational {
case lhs.den == rhs.den {
True -> Rational(lhs.num + rhs.num, lhs.den)
False ->
Rational({ lhs.num * rhs.den } + { lhs.den * rhs.num }, lhs.den * rhs.den)
}
}
pub fn subtract(lhs: Rational, rhs: Rational) -> Rational {
case lhs.den == rhs.den {
True -> Rational(lhs.num - rhs.num, lhs.den)
False ->
Rational({ lhs.num * rhs.den } - { lhs.den * rhs.num }, lhs.den * rhs.den)
}
}
pub fn multiply(lhs: Rational, rhs: Rational) -> Rational {
Rational(lhs.num * rhs.num, lhs.den * rhs.den)
}
/// Also a `new` for rationals.
///
/// Returns Error if the second rational's numerator is 0,
/// because you are dividing the first rational by zero.
pub fn divide_check(lhs: Rational, rhs: Rational) -> Result(Rational, Nil) {
flip_check(rhs) |> result.map(multiply(lhs, _))
}
/// Also a `new` for rationals.
///
/// Returns [`zero`](#zero) if the second rational's numerator is 0
/// because you are dividing the first rational by zero.
pub fn divide(lhs: Rational, rhs: Rational) -> Rational {
unwrap(divide_check(lhs, rhs))
}
/// Returns the largest of two rationals.
pub fn max(rat1: Rational, rat2: Rational) -> Rational {
// reversed so Eq is rat1
case compare(rat1, rat2) {
order.Lt -> rat2
_ -> rat1
}
}
/// Returns the smallest of two rationals.
pub fn min(rat1: Rational, rat2: Rational) -> Rational {
// reversed so Eq is rat1
case compare(rat1, rat2) {
order.Gt -> rat2
_ -> rat1
}
}
/// Restricts a rational between a lower and upper bound.
pub fn clamp(
rat: Rational,
min min_bound: Rational,
max max_bound: Rational,
) -> Rational {
rat |> min(max_bound) |> max(min_bound)
}
/// Returns Error if the numerator is 0 **and** the exponent is negative,
/// because the negative exponent makes it 1/0.
pub fn power_check(rat: Rational, of exponent: Int) -> Result(Rational, Nil) {
use num <- result.try(int_power_check(rat.num, exponent))
use den <- result.map(int_power_check(rat.den, exponent))
divide(num, den)
}
/// Returns [`zero`](#zero) if the numerator is 0 **and** the exponent is negative,
/// because the negative exponent makes it 1/0.
pub fn power(rat: Rational, of exponent: Int) -> Rational {
unwrap(power_check(rat, exponent))
}
/// Returns Error if the numerator is 0 **and** the exponent is negative,
/// because the negative exponent makes it 1/0.
pub fn int_power_check(base: Int, of exponent: Int) -> Result(Rational, Nil) {
case exponent >= 0 {
True -> util.int_power(base, exponent) |> from_int |> Ok
False -> new_check(1, util.int_power(base, int.negate(exponent)))
}
}
/// Returns [`zero`](#zero) if the numerator is 0 **and** the exponent is negative,
/// because the negative exponent makes it 1/0.
pub fn int_power(base: Int, of exponent: Int) -> Rational {
unwrap(int_power_check(base, exponent))
}
/// Multiplies a list of rationals and returns the product.
pub fn product(rats: List(Rational)) -> Rational {
list.fold(over: rats, from: one, with: multiply)
}
/// Sums a list of rationals.
pub fn sum(rats: List(Rational)) -> Rational {
list.fold(over: rats, from: one, with: add)
}
/// Splits the numerator and denominator into their digit representations in the specified base.
/// Returns an error if the base is less than 2.
pub fn digits(rat: Rational, base: Int) -> Result(#(List(Int), List(Int)), Nil) {
case int.digits(rat.num, base), int.digits(rat.den, base) {
Ok(num), Ok(den) -> Ok(#(num, den))
_, _ -> Error(Nil)
}
}
/// Joins two lists of digits into a rational.
///
/// Returns Error if the base is less than 2, the list contains
/// a digit greater than or equal to the specified base,
/// or the second list of digits equal zero.
pub fn undigits_check(
tup: #(List(Int), List(Int)),
base: Int,
) -> Result(Rational, Nil) {
do_undigits(tup, base) |> result.nil_error
}
/// Joins two lists of digits into a rational.
///
/// Returns an error if the base is less than 2, or the list contains
/// a digit greater than or equal to the specified base. \
/// Returns [`zero`](#zero) if the second list of digits equal zero.
pub fn undigits(
tup: #(List(Int), List(Int)),
base: Int,
) -> Result(Rational, Nil) {
case do_undigits(tup, base) {
Ok(rat) -> Ok(rat)
Error(ZeroError) -> Ok(zero)
_ -> Error(Nil)
}
}
fn do_undigits(
tup: #(List(Int), List(Int)),
base: Int,
) -> Result(Rational, UndigitsError) {
use num <- result.try(
int.undigits(tup.0, base) |> result.replace_error(BaseError),
)
use den <- result.try(
int.undigits(tup.1, base) |> result.replace_error(BaseError),
)
new_check(num, den) |> result.replace_error(ZeroError)
}
type UndigitsError {
BaseError
ZeroError
}
pub fn map_num(rat: Rational, fun: fn(Int) -> Int) -> Rational {
Rational(fun(rat.num), rat.den)
}
pub fn map_den_check(
rat: Rational,
fun: fn(Int) -> Int,
) -> Result(Rational, Nil) {
new_check(rat.num, fun(rat.den))
}
pub fn map_den(rat: Rational, fun: fn(Int) -> Int) -> Rational {
unwrap(map_den_check(rat, fun))
}
pub fn map_both_check(
rat: Rational,
fun: fn(Int) -> Int,
) -> Result(Rational, Nil) {
new_check(fun(rat.num), fun(rat.den))
}
pub fn map_both(rat: Rational, fun: fn(Int) -> Int) -> Rational {
unwrap(map_both_check(rat, fun))
}
pub fn map_seperate_check(
rat: Rational,
fun1: fn(Int) -> Int,
fun2: fn(Int) -> Int,
) -> Result(Rational, Nil) {
new_check(fun1(rat.num), fun2(rat.den))
}
pub fn map_seperate(
rat: Rational,
fun1: fn(Int) -> Int,
fun2: fn(Int) -> Int,
) -> Rational {
unwrap(map_seperate_check(rat, fun1, fun2))
}
/// Replace error with [`zero`](#zero).
pub fn unwrap(res: Result(Rational, Nil)) -> Rational {
result.unwrap(res, zero)
}
/// Internal function to shift negatives up off the denominator.
fn shift_negative(rat: Rational) {
case rat.den < 0 {
True -> Rational(int.negate(rat.num), int.negate(rat.den))
False -> rat
}
}