Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import re
from math import gcd
from .units_convert_data import UNITS, UnitDefinition ← this capability’s own data, compiled from data/units.json into the same file by fune build
DECIMAL = re.compile(r"^-?[0-9]+(\.[0-9]+)?$")
MAX_DIGITS = 15
def _unit(symbol: str) -> UnitDefinition:
for u in UNITS:
if u.symbol == symbol:
return u
raise ValueError('unknown unit "%s"' % (symbol,))
def convert_units(value: str, from_unit: str, to_unit: str, decimals: int) -> str:
"""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.
"""
if not isinstance(value, str) or not DECIMAL.fullmatch(value):
raise ValueError('value must be a plain decimal like "12.5", received "%s"' % (value,))
negative = value.startswith("-")
body = value[1:] if negative else value
whole, _, raw_fraction = body.partition(".")
fraction = raw_fraction.rstrip("0")
digits = (whole + fraction).lstrip("0")
if len(digits) > MAX_DIGITS or len(fraction) > MAX_DIGITS:
raise ValueError(
'value "%s" has too many digits: at most 15 significant digits and 15 decimal places' % (value,)
)
src = _unit(from_unit)
dst = _unit(to_unit)
if src.dimension != dst.dimension:
raise ValueError(
"cannot convert %s (%s) to %s (%s)" % (from_unit, src.dimension, to_unit, dst.dimension)
)
if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0 or decimals > 12:
raise ValueError("decimals must be a whole number from 0 to 12, received %s" % (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.
p = src.factor_numerator * dst.factor_denominator
q = src.factor_denominator * dst.factor_numerator
g = gcd(p, q)
p //= g
q //= g
r = (src.offset_numerator * dst.offset_denominator - dst.offset_numerator * src.offset_denominator) * dst.factor_denominator
s = src.offset_denominator * dst.offset_denominator * dst.factor_numerator
g = gcd(r, s)
if g != 0:
r //= g
s //= g
scale = 10 ** len(fraction)
magnitude = int(digits) if digits else 0
numerator = (-magnitude if negative else magnitude) * p * s + r * scale * q
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.
result_negative = numerator < 0
absolute = -numerator if result_negative else numerator
int_part, remainder = divmod(absolute, denominator)
frac_part = 0
for _ in range(decimals):
remainder *= 10
frac_part = frac_part * 10 + remainder // denominator
remainder %= denominator
if remainder * 2 >= denominator:
frac_part += 1
if frac_part == 10 ** decimals:
frac_part = 0
int_part += 1
text = str(int_part)
if decimals > 0:
shown = str(frac_part).rjust(decimals, "0").rstrip("0")
if shown:
text += "." + shown
if result_negative and (int_part != 0 or frac_part != 0):
text = "-" + text
return text