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(),
))
}