Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { UNITS, type UnitDefinition } from "./units_convert_data.ts"; ← this capability’s own data, compiled from data/units.json into the same file by fune build
const DECIMAL = /^-?[0-9]+(\.[0-9]+)?$/;
const MAX_DIGITS = 15;
function gcd(a: bigint, b: bigint): bigint {
let x = a < 0n ? -a : a;
let y = b < 0n ? -b : b;
while (y !== 0n) [x, y] = [y, x % y];
return x;
}
function unit(symbol: string): UnitDefinition {
const found = UNITS.find((u) => u.symbol === symbol);
if (!found) throw new RangeError(`unknown unit "${symbol}"`);
return found;
}
/**
* 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.
*/
export function convertUnits(value: string, fromUnit: string, toUnit: string, decimals: number): string {
if (typeof value !== "string" || !DECIMAL.test(value)) {
throw new RangeError(`value must be a plain decimal like "12.5", received "${value}"`);
}
const negative = value.startsWith("-");
const [whole, rawFraction = ""] = (negative ? value.slice(1) : value).split(".");
const fraction = rawFraction.replace(/0+$/, "");
const digits = (whole + fraction).replace(/^0+/, "");
if (digits.length > MAX_DIGITS || fraction.length > MAX_DIGITS) {
throw new RangeError(`value "${value}" has too many digits: at most 15 significant digits and 15 decimal places`);
}
const from = unit(fromUnit);
const to = unit(toUnit);
if (from.dimension !== to.dimension) {
throw new RangeError(`cannot convert ${fromUnit} (${from.dimension}) to ${toUnit} (${to.dimension})`);
}
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 12) {
throw new RangeError(`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 p = BigInt(from.factorNumerator) * BigInt(to.factorDenominator);
let q = BigInt(from.factorDenominator) * BigInt(to.factorNumerator);
let g = gcd(p, q);
p /= g;
q /= g;
let r =
(BigInt(from.offsetNumerator) * BigInt(to.offsetDenominator) - BigInt(to.offsetNumerator) * BigInt(from.offsetDenominator)) *
BigInt(to.factorDenominator);
let s = BigInt(from.offsetDenominator) * BigInt(to.offsetDenominator) * BigInt(to.factorNumerator);
g = gcd(r, s);
if (g !== 0n) {
r /= g;
s /= g;
}
const scale = 10n ** BigInt(fraction.length);
const magnitude = BigInt(digits === "" ? "0" : digits);
const numerator = (negative ? -magnitude : magnitude) * p * s + r * scale * q;
const denominator = scale * q * s;
// Long division one digit at a time keeps every intermediate below 10 x the
// denominator, which is what lets the Rust port do the same in i128.
const resultNegative = numerator < 0n;
const absolute = resultNegative ? -numerator : numerator;
let intPart = absolute / denominator;
let remainder = absolute % denominator;
let fracPart = 0n;
for (let i = 0; i < decimals; i++) {
remainder *= 10n;
fracPart = fracPart * 10n + remainder / denominator;
remainder %= denominator;
}
if (remainder * 2n >= denominator) {
fracPart += 1n;
if (fracPart === 10n ** BigInt(decimals)) {
fracPart = 0n;
intPart += 1n;
}
}
let text = intPart.toString();
if (decimals > 0) {
const shown = fracPart.toString().padStart(decimals, "0").replace(/0+$/, "");
if (shown !== "") text += "." + shown;
}
if (resultNegative && (intPart !== 0n || fracPart !== 0n)) text = "-" + text;
return text;
}