Functional Weave
Code in Python

math.rational@1.0.0

impl/rust.rs

3,979 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_gcd_lcm::gcd_wide;  ← from math.gcd-lcm ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

/// Reduce a wide fraction and bring it back into the exact number range.
fn reduce(numerator: i128, denominator: i128) -> Rational {
    if denominator == 0 {
        panic!("denominator must not be zero");
    }
    let g = gcd_wide(numerator, denominator);
    let mut n = numerator / g;
    let mut d = denominator / g;
    if d < 0 {
        n = -n;
        d = -d;
    }
    // i64 could hold more, but TypeScript numbers stop being exact here, and
    // the three languages must agree.
    if n > MAX_SAFE || n < -MAX_SAFE || d > MAX_SAFE {
        panic!("rational overflow: the reduced result exceeds 2^53 - 1");
    }
    Rational {
        numerator: n as i64,
        denominator: d as i64,
    }
}

/// Build a reduced fraction with a positive denominator.
///
/// # Panics
/// Panics if the denominator is zero or either part is outside ±(2^53 - 1).
pub fn rational(numerator: i64, denominator: i64) -> Rational {
    let (n, d) = (numerator as i128, denominator as i128);
    if n > MAX_SAFE || n < -MAX_SAFE || d > MAX_SAFE || d < -MAX_SAFE {
        panic!("rational overflow: numerator and denominator must be within 2^53 - 1");
    }
    reduce(n, d)
}

fn parts(r: &Rational) -> (i128, i128) {
    let n = rational(r.numerator, r.denominator);
    (n.numerator as i128, n.denominator as i128)
}

pub fn add_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    reduce(an * bd + bn * ad, ad * bd)
}

pub fn subtract_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    reduce(an * bd - bn * ad, ad * bd)
}

pub fn multiply_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    reduce(an * bn, ad * bd)
}

/// # Panics
/// Panics if `b` is zero.
pub fn divide_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    if bn == 0 {
        panic!("division by zero");
    }
    reduce(an * bd, ad * bn)
}

/// -1, 0 or 1. Exact: cross-multiplied in i128, never through a float.
pub fn compare_rational(a: &Rational, b: &Rational) -> i64 {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    match (an * bd).cmp(&(bn * ad)) {
        std::cmp::Ordering::Less => -1,
        std::cmp::Ordering::Equal => 0,
        std::cmp::Ordering::Greater => 1,
    }
}

/// The nearest integer under an explicit rounding mode.
pub fn rational_to_integer(r: &Rational, mode: &str) -> i64 {
    let n = rational(r.numerator, r.denominator);
    round_div(n.numerator, n.denominator, mode)
}

/// Add, subtract, multiply or divide two fractions exactly.
///
/// The result is always reduced with a positive denominator, so equal values
/// have equal fields.
///
/// # Panics
/// Panics on a zero denominator, division by zero, an unknown operation, or a
/// result outside ±(2^53 - 1).
pub fn calculate_rational(a: &Rational, op: &str, b: &Rational) -> Rational {
    match op {
        "add" => add_rational(a, b),
        "subtract" => subtract_rational(a, b),
        "multiply" => multiply_rational(a, b),
        "divide" => divide_rational(a, b),
        other => panic!("unknown operation \"{}\"", other),
    }
}

pub fn rational_to_value(r: &Rational) -> Value {
    Value::obj(vec![
        ("numerator", Value::Int(r.numerator)),
        ("denominator", Value::Int(r.denominator)),
    ])
}

pub fn rational_from_value(v: &Value) -> Rational {
    Rational {
        numerator: v.get("numerator").as_i64(),
        denominator: v.get("denominator").as_i64(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    rational_to_value(&calculate_rational(
        &rational_from_value(&args[0]),
        args[1].as_str(),
        &rational_from_value(&args[2]),
    ))
}