Functional Weave
Code in Rust

units.convert@1.0.0

impl/rust.rs

5,516 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::units_convert_data::{UnitDefinition, UNITS};  ← this capability’s own data, compiled from data/units.json into the same file by fune build

const MAX_DIGITS: usize = 15;

fn gcd(a: i128, b: i128) -> i128 {
    let (mut x, mut y) = (a.abs(), b.abs());
    while y != 0 {
        let t = x % y;
        x = y;
        y = t;
    }
    x
}

fn unit(symbol: &str) -> &'static UnitDefinition {
    match UNITS.iter().find(|u| u.symbol == symbol) {
        Some(found) => found,
        None => panic!("unknown unit \"{}\"", symbol),
    }
}

fn is_decimal(value: &str) -> bool {
    let body = value.strip_prefix('-').unwrap_or(value);
    let mut parts = body.splitn(2, '.');
    let whole = parts.next().unwrap_or("");
    let fraction = parts.next();
    let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
    digits(whole) && fraction.map_or(true, digits)
}

// The inputs are capped at 15 significant digits, and the largest reduced
// factor over every pair in the table is 1.8e17 (MWh to Btu), so no product
// here exceeds about 2e33; checked arithmetic makes any future table entry
// that breaks that bound fail loudly instead of wrapping.
fn mul(a: i128, b: i128) -> i128 {
    a.checked_mul(b)
        .unwrap_or_else(|| panic!("value too large to convert exactly"))
}

/// Convert a quantity between units of the same dimension, exactly.
///
/// The value travels as decimal text and every factor is an exact fraction
/// (1 in = 254/10000 m, 1 lb = 45359237/100000000 kg), so the only rounding
/// is the one at the end, to `decimals` places. Multiplying floats instead
/// gets 0.75 in = 19.049999999999997 mm, which rounds to 19.0 rather than 19.1.
///
/// # Panics
/// Panics on malformed or over-long values, unknown units, a dimension
/// mismatch, or `decimals` outside 0..=12.
pub fn convert_units(value: &str, from_unit: &str, to_unit: &str, decimals: i64) -> String {
    if !is_decimal(value) {
        panic!(
            "value must be a plain decimal like \"12.5\", received \"{}\"",
            value
        );
    }
    let negative = value.starts_with('-');
    let body = if negative { &value[1..] } else { value };
    let mut parts = body.splitn(2, '.');
    let whole = parts.next().unwrap_or("");
    let fraction = parts.next().unwrap_or("").trim_end_matches('0');
    let joined = format!("{}{}", whole, fraction);
    let digits = joined.trim_start_matches('0');
    if digits.len() > MAX_DIGITS || fraction.len() > MAX_DIGITS {
        panic!(
            "value \"{}\" has too many digits: at most 15 significant digits and 15 decimal places",
            value
        );
    }

    let from = unit(from_unit);
    let to = unit(to_unit);
    if from.dimension != to.dimension {
        panic!(
            "cannot convert {} ({}) to {} ({})",
            from_unit, from.dimension, to_unit, to.dimension
        );
    }
    if !(0..=12).contains(&decimals) {
        panic!(
            "decimals must be a whole number from 0 to 12, received {}",
            decimals
        );
    }

    // base = v * fa + oa, result = (base - ob) / fb
    //      = v * P/Q + R/S, with P/Q = fa/fb and R/S = (oa - ob)/fb, each reduced.
    let mut p = mul(from.factor_numerator as i128, to.factor_denominator as i128);
    let mut q = mul(from.factor_denominator as i128, to.factor_numerator as i128);
    let g = gcd(p, q);
    p /= g;
    q /= g;
    let mut r = mul(
        mul(from.offset_numerator as i128, to.offset_denominator as i128)
            - mul(to.offset_numerator as i128, from.offset_denominator as i128),
        to.factor_denominator as i128,
    );
    let mut s = mul(
        mul(from.offset_denominator as i128, to.offset_denominator as i128),
        to.factor_numerator as i128,
    );
    let g = gcd(r, s);
    if g != 0 {
        r /= g;
        s /= g;
    }

    let scale = 10i128.pow(fraction.len() as u32);
    let magnitude: i128 = if digits.is_empty() { 0 } else { digits.parse().unwrap() };
    let signed = if negative { -magnitude } else { magnitude };
    let numerator = mul(mul(signed, p), s)
        .checked_add(mul(mul(r, scale), q))
        .unwrap_or_else(|| panic!("value too large to convert exactly"));
    let denominator = mul(mul(scale, q), s);

    // Long division one digit at a time keeps every intermediate below 10 x
    // the denominator, so i128 is enough for every pair in the table.
    let result_negative = numerator < 0;
    let absolute = numerator.abs();
    let mut int_part = absolute / denominator;
    let mut remainder = absolute % denominator;
    let mut frac_part: i128 = 0;
    for _ in 0..decimals {
        remainder = mul(remainder, 10);
        frac_part = frac_part * 10 + remainder / denominator;
        remainder %= denominator;
    }
    if mul(remainder, 2) >= denominator {
        frac_part += 1;
        if frac_part == 10i128.pow(decimals as u32) {
            frac_part = 0;
            int_part += 1;
        }
    }

    let mut text = int_part.to_string();
    if decimals > 0 {
        let padded = format!("{:0>width$}", frac_part, width = decimals as usize);
        let shown = padded.trim_end_matches('0');
        if !shown.is_empty() {
            text.push('.');
            text.push_str(shown);
        }
    }
    if result_negative && (int_part != 0 || frac_part != 0) {
        text.insert(0, '-');
    }
    text
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::Str(convert_units(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_i64(),
    ))
}