use super::funejson::Value; use super::units_convert_data::{UnitDefinition, UNITS}; 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(), )) }