diff --git a/assets/Amount of substance.ico b/assets/Amount of substance.ico new file mode 100644 index 0000000..2ca20be Binary files /dev/null and b/assets/Amount of substance.ico differ diff --git a/assets/Angle.ico b/assets/Angle.ico new file mode 100644 index 0000000..3d88a7d Binary files /dev/null and b/assets/Angle.ico differ diff --git a/assets/Density.ico b/assets/Density.ico new file mode 100644 index 0000000..7e33b3f Binary files /dev/null and b/assets/Density.ico differ diff --git a/assets/Dose equivalent.ico b/assets/Dose equivalent.ico new file mode 100644 index 0000000..f7303dd Binary files /dev/null and b/assets/Dose equivalent.ico differ diff --git a/assets/Dynamic viscosity.ico b/assets/Dynamic viscosity.ico new file mode 100644 index 0000000..ab446aa Binary files /dev/null and b/assets/Dynamic viscosity.ico differ diff --git a/assets/Force.ico b/assets/Force.ico new file mode 100644 index 0000000..ba43f2d Binary files /dev/null and b/assets/Force.ico differ diff --git a/assets/Frequency.ico b/assets/Frequency.ico new file mode 100644 index 0000000..f9311b4 Binary files /dev/null and b/assets/Frequency.ico differ diff --git a/assets/Kinematic viscosity.ico b/assets/Kinematic viscosity.ico new file mode 100644 index 0000000..4a3bccc Binary files /dev/null and b/assets/Kinematic viscosity.ico differ diff --git a/assets/Power.ico b/assets/Power.ico new file mode 100644 index 0000000..9cfde2d Binary files /dev/null and b/assets/Power.ico differ diff --git a/assets/Radiation dose.ico b/assets/Radiation dose.ico new file mode 100644 index 0000000..3650d43 Binary files /dev/null and b/assets/Radiation dose.ico differ diff --git a/assets/Radioactivity.ico b/assets/Radioactivity.ico new file mode 100644 index 0000000..c4a6d10 Binary files /dev/null and b/assets/Radioactivity.ico differ diff --git a/assets/Torque.ico b/assets/Torque.ico new file mode 100644 index 0000000..a0b557e Binary files /dev/null and b/assets/Torque.ico differ diff --git a/plugin/general_converter.py b/plugin/general_converter.py index c37e528..1d8cbfe 100644 --- a/plugin/general_converter.py +++ b/plugin/general_converter.py @@ -1,224 +1,166 @@ import locale import textwrap import re +import subprocess import units as gc_units from translation import _ from flox import Flox +locale.setlocale(locale.LC_NUMERIC, "") + + +# --------------------------------------------------------------------------- +# Unit lookup helpers +# --------------------------------------------------------------------------- + +def _find_unit(abbr: str): + """Return (category_key, unit_tuple) or (None, None).""" + for cat_key, cat in gc_units.units.items(): + for u in cat["units"]: + if abbr in (u[0], u[1], u[2]): + return cat_key, u + return None, None + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def get_all_units(short: bool = False) -> list: + """Return [[category, unit, ...], ...] — index 0 is always the category name.""" + result = [] + for cat_key, cat in gc_units.units.items(): + row = [cat_key] + [u[0] if short else f"{u[1]} ({u[0]})" for u in cat["units"]] + result.append(row) + return result + + +def get_hints_for_category(from_unit: str) -> list: + """Return abbreviations of all other units in the same category.""" + cat_key, _ = _find_unit(from_unit) + if not cat_key: + return [_("no valid units")] + return [u[0] for u in gc_units.units[cat_key]["units"] if u[0] != from_unit] + + +def gen_convert(amount: float, from_unit: str, to_unit: str) -> dict: + """Convert amount from from_unit to to_unit. + + Returns a result dict on success or {"Error": reason} on failure. + """ + if from_unit == to_unit: + return {"Error": _("To and from unit is the same")} + + from_cat, src = _find_unit(from_unit) + to_cat, dst = _find_unit(to_unit) + + if not src or not dst: + return {"Error": _("Problem converting {} and {}").format(from_unit, to_unit)} + if from_cat != to_cat: + return {"Error": _("Units are from different categories")} + + return { + "category": from_cat, + "converted": gc_units.convert(amount, src[0], dst[0], from_cat), + "fromabbrev": src[0], + "fromlong": src[1], + "fromplural": src[2], + "toabbrev": dst[0], + "tolong": dst[1], + "toplural": dst[2], + } + + +def smart_precision(separator: str, amount: float, preferred: int = 3) -> int: + """Return an appropriate number of decimal places for display.""" + s = str(amount) + dec_places = s[::-1].find(separator) + if dec_places == -1: + return 0 + frac = s[-dec_places:] + if int(frac) == 0: + return 0 + fnz = re.search(r"[1-9]", frac).start() + return preferred if fnz < preferred else fnz + 1 + + +# --------------------------------------------------------------------------- +# Flox plugin +# --------------------------------------------------------------------------- class GenConvert(Flox): - locale.setlocale(locale.LC_NUMERIC, "") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.logger_level("info") def query(self, query): - q = query.strip() - args = q.split(" ") - # Just keyword - show all units if the setting allows + args = query.strip().split() + if len(args) == 1: - all_units = get_all_units() - self.add_item( - title=_("General Converter"), - subtitle=_( - " " - ), - ) - if self.settings.get("show_helper_text"): - for cat in all_units: - title = str(cat[0]) - subtitle = ", ".join([str(elem) for elem in cat[1:]]) - lines = textwrap.wrap(subtitle, 110, break_long_words=False) - if len(lines) > 1: - self.add_item( - title=(title), - subtitle=(lines[0]), - icon=f"assets/{title}.ico", - ) - for line in range(1, len(lines)): - self.add_item( - title=(title), - subtitle=(lines[line]), - icon=f"assets/{title}.ico", - ) - else: - self.add_item( - title=(title), - subtitle=(subtitle), - icon=f"assets/{title}.ico", - ) - # Keyword and first unit to convert from - show what it can be converted to + self._show_all_units() elif len(args) == 2: hints = get_hints_for_category(args[1]) self.add_item( title=_("Available conversions"), - subtitle=(f"{args[0]} {args[1]} to {', '.join(hints)}"), + subtitle=f"{args[0]} {args[1]} to {', '.join(hints)}", ) - # Keyword and two units to convert from and to - try to convert elif len(args) == 3: - try: - # Units are case sensitive. - do_convert = gen_convert(float(args[0]), args[1], args[2]) - if "Error" in do_convert: - if do_convert["Error"] == _("To and from unit is the same"): - self.add_item( - title=_("{}".format(do_convert["Error"])), - subtitle=_("Choose two different units"), - ) - else: - self.add_item( - # f strings seem to break babel so use string formatting instead - title=_("Error - {}").format(do_convert["Error"]), - subtitle=_("Check documentation for accepted units"), - ) - else: - converted = do_convert["converted"] - category = do_convert["category"] - to_long = do_convert["toplural"] - to_abb = do_convert["toabbrev"] - from_long = do_convert["fromplural"] - from_abb = do_convert["fromabbrev"] - converted_precision = smart_precision( - locale.localeconv()["decimal_point"], converted, 3 - ) - self.add_item( - title=(category), - subtitle=( - f"{locale.format_string('%.10g', float(args[0]), grouping=True)} {from_long} ({from_abb}) = {locale.format_string(f'%.{converted_precision}f', converted, grouping=True)} {to_long} ({to_abb})" - ), - icon=f"assets/{do_convert['category']}.ico", - ) - do_convert = [] - except Exception as e: - self.add_item(title="Error - {}").format(repr(e), subtitle="") - # Always show the usage while there isn't a valid query + self._do_convert(*args) else: self.add_item( title=_("General Converter"), subtitle=_(" "), ) - -def get_all_units(short: bool = False): - """Returns all available units as a list of lists by category - - :param short: if True only unit abbreviations are returned, default is False - :type amount: bool - - :rtype: list of lists - :return: A list of lists for each category in units. Index 0 of each internal list - is the category description - """ - - full_list = [] - for u in gc_units.units: - cat_list = [] - cat_list.append(u) - for u2 in gc_units.units[u]: - cat_list.append(u2[0] if short else f"{u2[1]} ({u2[0]})") - full_list.append(cat_list) - return full_list - - -def get_hints_for_category(from_unit: str): - """Takes an input unit and returns a list of units it can be converted to - - :param from_short: unit abbreviation - :type amount: str - - :rtype: list - :return: A list of other unit abbreviations in the same category - """ - c = [] - category = "" - - # Find the category it's in - for u in gc_units.units: - for u2 in gc_units.units[u]: - if u2[0] == from_unit or u2[1] == from_unit or u2[2] == from_unit: - category = str(u) - for uu in gc_units.units[category]: - if uu[0] != from_unit: - c.append(uu[0]) - if category: - return c - else: - return ["no valid units"] - - -def gen_convert(amount: float, from_unit: str, to_unit: str): - """Converts from one unit to another - - :param amount: amount of source unit to convert - :type amount: float - :param from_unit: abbreviation of unit to convert from - :type from_unit: str - :param to_unit: abbreviation of unit to convert to - :type to_unit: str - - :rtype: dict - :return: if to_unit and from_unit are valid returns a dictionary - { - "category":{category of units}, - "converted":{converted amount}, - "fromabbrev":{from unit abbreviation}, - "fromlong":{from unit long name}, - "fromplural":{from unit plural name}, - "toabbrev":{to unit abbreviation}, - "tolong":{to unit long name}, - "toplural":{to unit plural name}, - } - - else returns a dictionary with error status - {"Error": {error text}} - """ - conversions = {} - found_from = found_to = [] - if from_unit == to_unit: - conversions["Error"] = _("To and from unit is the same") - return conversions - for u in gc_units.units: - for u2 in gc_units.units[u]: - if u2[0] == from_unit or u2[1] == from_unit or u2[2] == from_unit: - found_from = u2 - if u2[0] == to_unit or u2[1] == to_unit or u2[2] == to_unit: - found_to = u2 - # If we haven't both in the same category, reset - if found_to and found_from: - found_category = u - break - else: - found_from = found_to = [] - if found_to and found_from: - base_unit_conversion = eval(found_from[3].replace("x", str(amount))) - final_conversion = eval(found_to[4].replace("x", str(base_unit_conversion))) - conversions["category"] = found_category - conversions["converted"] = final_conversion - conversions["fromabbrev"] = found_from[0] - conversions["fromlong"] = found_from[1] - conversions["fromplural"] = found_from[2] - conversions["toabbrev"] = found_to[0] - conversions["tolong"] = found_to[1] - conversions["toplural"] = found_to[2] - - else: - conversions["Error"] = "Problem converting {} and {}".format(from_unit, to_unit) - return conversions - - -def smart_precision(separator, amount, preferred=3): - str_amt = str(amount) - dec_places = str_amt[::-1].find(separator) - # whole number - if dec_places == -1: - return 0 - frac_part = str_amt[-dec_places::] - # fraction is just zeroes - if int(frac_part) == 0: - return 0 - fnz = re.search(r"[1-9]", frac_part).start() - if fnz < preferred: - return preferred - return fnz + 1 + def _show_all_units(self): + self.add_item( + title=_("General Converter"), + subtitle=_(" "), + ) + if not self.settings.get("show_helper_text"): + return + for cat in get_all_units(): + title = str(cat[0]) + subtitle = ", ".join(str(e) for e in cat[1:]) + icon = f"assets/{title}.ico" + for line in textwrap.wrap(subtitle, 110, break_long_words=False) or [subtitle]: + self.add_item(title=title, subtitle=line, icon=icon) + + def _do_convert(self, raw_amount: str, from_unit: str, to_unit: str): + try: + result = gen_convert(float(raw_amount), from_unit, to_unit) + except ValueError: + self.add_item(title=_("Error - invalid number"), subtitle=raw_amount) + return + + if "Error" in result: + err = result["Error"] + sub = (_("Choose two different units") + if err == _("To and from unit is the same") + else _("Check documentation for accepted units")) + self.add_item(title=err, subtitle=sub) + return + + dp = locale.localeconv()["decimal_point"] + precision = smart_precision(dp, result["converted"], 3) + amount_fmt = locale.format_string("%.10g", float(raw_amount), grouping=True) + converted_fmt = locale.format_string(f"%.{precision}f", result["converted"], grouping=True) + copy_value = locale.format_string(f"%.{precision}f", result["converted"]) # no thousands sep + + self.add_item( + title=result["category"], + subtitle=( + f"{amount_fmt} {result['fromplural']} ({result['fromabbrev']}) = " + f"{converted_fmt} {result['toplural']} ({result['toabbrev']})" + f" [Enter copies value]" + ), + icon=f"assets/{result['category']}.ico", + method=self.copy_to_clipboard, + parameters=[copy_value], + ) + + def copy_to_clipboard(self, value: str): + """Copy converted value to Windows clipboard via clip.exe.""" + subprocess.run(["clip"], input=value.encode("utf-16-le"), check=True) diff --git a/plugin/units.py b/plugin/units.py index cc87acc..3cc560a 100644 --- a/plugin/units.py +++ b/plugin/units.py @@ -1,459 +1,386 @@ from translation import _ -""" -Unit Syntax: -units = { - "Category description": [ - ["abbreviation", - "unit name (singular)", - "unit name (plural)", - "conversion formula TO base unit", - "conversion formula FROM base unit"] - ] -} +# --------------------------------------------------------------------------- +# Unit definitions +# --------------------------------------------------------------------------- +# Format — linear units (4 fields): +# (abbr, singular, plural, factor) +# factor = how many base units is 1 of this unit +# to_base = value * factor +# from_base = value / factor +# +# Format — affine units (5 fields, e.g. Temperature): +# (abbr, singular, plural, factor, offset) +# to_base = (value + offset) * factor +# from_base = (value / factor) - offset +# +# The first entry in each "units" list is the base unit (factor=1). +# --------------------------------------------------------------------------- -The first entry for each category in the list of lists MUST be the base unit. -Descriptions wrapped in '_()' can, and should, be translated via the Python template -translation methods -""" units = { - _("Distance"): [ - # Base - ["m", _("metre"), _("metres"), "x * 1", "x * 1"], - # All below convert to / from base - ["dm", _("decimetre"), _("decimetres"), "x / 10", "x * 10"], - ["mm", _("millimetre"), _("millimetres"), "x / 1000", "x * 1000"], - ["cm", _("centimetre"), _("centimetres"), "x / 100", "x * 100"], - ["km", _("kilometre"), _("kilometres"), "x / 0.001", "x * 0.001"], - ["in", _("inch"), _("inches"), "x / 39.37008", "x * 39.37008"], - ["ft", _("foot"), _("feet"), "x / 3.28084", "x * 3.28084"], - ["yd", _("yard"), _("yards"), "x / 1.093613", "x * 1.093613"], - ["mi", _("mile"), _("miles"), "x / 0.0006213712", "x * 0.0006213712"], - ], - _("Volume"): [ - # NOTE :- Volumes are tricky because America. Non metric units often have a US and Imperial - # version. The default is US and the Imperial conversion can often be accessed by adding "imp" - # to the unit code. - # Base - ["ml", _("millilitre"), _("millilitres"), "x * 1", "x * 1"], - # All below convert to/from base - ["g", _("gram"), _("grams"), "x * 1", "x * 1"], - ["l", _("litre"), _("litres"), "x / 0.001", "x * 0.001"], - ["decal", _("decalitre"), _("decalitres"), "x / 0.0001", "x * 0.0001"], - ["pt", _("pint US"), _("pints US"), "x / 0.002113383", "x * 0.002113383"], - [ - "ptimp", - _("pint Imperial"), - _("pints Imperial"), - "x / 0.001759754", - "x * 0.001759754", - ], - ["qt", _("quart US"), _("quarts US"), "x / 0.001056691", "x * 0.001056691"], - [ - "qtimp", - _("quart Imperial"), - _("quarts Imperial"), - "x / 0.000879877", - "x * 0.000879877", - ], - [ - "cup", - _("cup US"), - _("cups US"), - "x / 0.0042267528198649", - "x * 0.0042267528198649", - ], - [ - "cupimp", - _("cup Imperial"), - _("cups Imperial"), - "x / 0.003519508", - "x * 0.003519508", - ], - [ - "tbsp", - _("tablespoon US"), - _("tabelspoons US"), - "x / 0.067628224", - "x * 0.067628224", - ], - [ - "tbspimp", - _("tablespoon Imperial"), - _("tabelspoons Imperial"), - "x / 0.05631213", - "x * 0.05631213", - ], - [ - "tsp", - _("teaspoon US"), - _("teaspoons US"), - "x / 0.2028846715942", - "x * 0.2028846715942", - ], - [ - "tspimp", - _("teaspoon Imperial"), - _("teaspoons Imperial"), - "x / 0.1689364", - "x * 0.1689364", - ], - [ - "gal", - _("gallon US"), - _("gallons US"), - "x / 0.0002641727499999601", - "x * 0.0002641727499999601", - ], - [ - "galimp", - _("gallon Imperial"), - _("gallons Imperial"), - "x / 0.0002199692", - "x * 0.0002199692", - ], - [ - "floz", - _("fluid ounce US"), - _("fluid ounces US"), - "x / 0.03381413", - "x * 0.03381413", - ], - [ - "flozimp", - _("fluid ounce Imperial"), - _("fluid ounces Imperial"), - "x / 0.03519508", - "x * 0.03519508", - ], - [ - "dm3", - _("cubic decimetre"), - _("cubic decimetres"), - "x / 0.001", - "x * 0.001", - ], - [ - "mm3", - _("cubic millimetre"), - _("cubic millimetres"), - "x / 0.1000", - "x * 0.1000", - ], - [ - "cm3", - _("cubic centimetre"), - _("cubic centimetres"), - "x / 1", - "x * 1", - ], - [ - "m3", - _("cubic metre"), - _("cubic metres"), - "x / 0.000001", - "x * 0.000001", - ], - [ - "in3", - _("cubic inch"), - _("cubic inches"), - "x / 0.061024", - "x * 0.061024", - ], - [ - "ft3", - _("cubic feet"), - _("cubic feet"), - "x / 0.0000353147", - "x * 0.0000353147", - ], - [ - "buuk", - _("bushel UK"), - _("bushels UK"), - "x / 0.0000274961", - "x * 0.0000274961", - ], - [ - "buus", - _("bushel US"), - _("bushels US"), - "x / 0.0000283776", - "x * 0.0000283776", - ], - ], - "Area": [ - # Base - ["sqm", _("square metre"), _("square metres"), "x * 1", "x * 1"], - ["h", _("hectare"), _("hectares"), "x / 0.0001", "x * 0.0001"], - ["ac", _("acre"), _("acres"), "x / 0.0002471052", "x * 0.0002471052"], - [ - "sqcm", - _("square centimetre"), - _("square centimetres"), - "x / 10000", - "x * 10000", - ], - [ - "sqkm", - _("square kilometre"), - _("square kilometres"), - "x / 1000000", - "x * 1000000", - ], - ["sqin", _("square inch"), _("square inches"), "x / 1550.003", "x * 1550.003"], - [ - "sqmi", - _("square mile"), - _("square miles"), - "x / 0.0000003861022", - "x * 0.0000003861022", - ], - ["sqft", _("square foot"), _("square feet"), "x / 10.76391", "x * 10.76391"], - ["sqyd", _("square yard"), _("square yards"), "x / 1.19599", "x * 1.19599"], - ], - "Weight": [ - # Base - ["g", _("gram"), _("grams"), "x * 1", "x * 1"], - # All below convert to/from base - ["kg", _("kilogram"), _("kilograms"), "x / 0.001", "x * 0.001"], - ["lb", _("pound"), _("pounds"), "x / 0.002205", "x * 0.002205"], - ["oz", _("ounce"), _("ounces"), "x / 0.035274", "x * 0.035274"], - ["st", _("stone"), _("stone"), "x / 0.000157473", "x * 0.000157473"], - [ - "t", - _("tonne"), - _("tonnes"), - "x / 0.000001", - "x * 0.000001", - ], - [ - "ton", - _("US ton"), - _("US tons"), - "x / 0.0000011023109950010196", - "x * 0.0000011023109950010196", - ], - [ - "tonimp", - _("Imperial ton"), - _("Imperial tons"), - "x / 0.00000098421", - "x * 0.00000098421", - ], - ], - "Temperature": [ - # Base - ["c", _("Celsius"), _("Celsius"), "x * 1", "x * 1"], - # All below convert to/from base - ["f", _("Farenheit"), _("Farenheit"), "(x - 32) / 1.8", "(x * 1.8) + 32"], - ["k", _("Kelvin"), _("Kelvin"), "x - 273.15", "x + 273.15"], - ], - "Speed": [ - # Base - ["km/h", _("kilometres per hour"), _("kilometres per hour"), "x * 1", "x * 1"], - [ - "m/s", - _("metres per second"), - _("metres per second"), - "x / 0.2777777778", - "x * 0.2777777778", - ], - [ - "mp/h", - _("miles per hour"), - _("miles per hour"), - "x / 0.6213711922", - "x * 0.6213711922", - ], - [ - "kt", - _("knot"), - _("knots"), - "x / 0.5399568035", - "x * 0.5399568035", - ], - ], - "Energy": [ - # Base - ["J", _("joule"), _("joules"), "x * 1", "x * 1"], - [ - "cal", - _("calorie"), - _("calories"), - "x / 0.2388459", - "x * 0.2388459", - ], - [ - "kcal", - _("kilocalorie"), - _("kilocalories"), - "x / 0.0002388459", - "x * 0.0002388459", - ], - [ - "kJ", - _("kilojoule"), - _("kilojoules"), - "x / 0.001", - "x * 0.001", - ], - [ - "MJ", - _("megajoule"), - _("megajoules"), - "x / 0.000001", - "x * 0.000001", - ], - [ - "Gj", - _("gigajoule"), - _("gigajoules"), - "x / 0.0000000010", - "x * 0.0000000010", - ], - [ - "kWh", - _("kilowatt hour"), - _("kilowatt hours"), - "x / 0.0000002778", - "x * 0.0000002778", - ], - [ - "BTU", - _("British thermal unit"), - _("British thermal units"), - "x / 0.0009478171", - "x * 0.0009478171", - ], - ], - "data": [ - # byte is the base - ["B", _("byte"), _("bytes"), "x / 1", "x * 1"], - ["KB", _("kilobyte"), _("kilobytes"), "x / 0.001", "x * 0.001"], # 10^3, - ["MB", _("megabyte"), _("megabytes"), "x / 0.000001", "x * 0.000001"], # 10^6, - [ - "GB", - _("gigabyte"), - _("gigabytes"), - "x / 0.000000001", - "x * 0.000000001", - ], # 10^9, - [ - "TB", - _("terabyte"), - _("terabytes"), - "x / 0.000000000001", - "x * 0.000000000001", - ], # 10^12, - [ - "PB", - _("petabyte"), - _("petabytes"), - "x / 0.000000000000001", - "x * 0.000000000000001", - ], # 10^15, - # bits - ["b", _("bit"), _("bits"), "x / 8", "x * 8"], # 1 byte = 8 bits - ["Kb", _("kilobit"), _("kilobits"), "x / 0.008 ", "x * 0.008"], - ["Mb", _("megabit"), _("megabits"), "x / 0.000008", "x * 0.000008"], - ["Gb", _("gigabit"), _("gigabits"), "x / 0.000000008", "x * 0.000000008"], - [ - "Tb", - _("terabit"), - _("terabits"), - "x / 0.000000000008", - "x * 0.000000000008", - ], - [ - "Pb", - _("petabit"), - _("petabits"), - "x / 0.000000000000008", - "x * 0.000000000000008", - ], - # IEC units - # bytes - [ - "KiB", - _("kibibyte"), - _("kibibytes"), - "x / 0.0009765625", - "x * 0.0009765625", - ], # 2^10 - [ - "MiB", - _("mebibyte"), - _("mebibytes"), - "x / 0.00000095367431640625", - "x * 0.00000095367431640625", - ], # 2^20 - [ - "GiB", - _("gibibyte"), - _("gibibytes"), - "x / 0.000000000931322574615478515625", - "x * 0.000000000931322574615478515625", - ], # 2^30 - [ - "TiB", - _("tebibyte"), - _("tebibytes"), - "x / 0.0000000000009094947017729282379150390625", - "x * 0.0000000000009094947017729282379150390625", - ], # 2^40 - [ - "PiB", - _("pebibyte"), - _("pebibytes"), - "x / 0.00000000000000088817841970012523233890533447265625", - "x * 0.00000000000000088817841970012523233890533447265625", - ], # 2^50 - # bits each is 1024 of the previous - [ - "Kib", - _("kibibit"), - _("kibibits"), - "x / 0.0078125", - "x * 0.0078125", - ], # 2^10 - [ - "Mib", - _("mebibit"), - _("mebibits"), - "x / 0.00000762939453125", - "x * 0.00000762939453125", - ], # 2^20 - [ - "Gib", - _("gibibit"), - _("gibibits"), - "x / 0.000000007450581", - "x * 0.000000007450581", - ], # 2^30 - [ - "Tib", - _("tebibit"), - _("tebibits"), - "x / 0.0000000000007275958", - "x * 0.0000000000007275958", - ], # 2^40 - [ - "Pib", - _("pebibit"), - _("pebibits"), - "x / 0.00000000000000000710542735760100185871124267578125", - "x * 0.00000000000000000710542735760100185871124267578125", - ], # 2^50 - ], - "Pressure": [ - # Base - ["bar", _("bar"), _("bar"), "x * 1", "x * 1"], - # All below convert to / from base - ["psi", _("psi"), _("psi"), "x / 14.5038", "x * 14.5038"], - ["atm", _("atm"), _("atm"), "x * 1.01325", "x / 1.01325"], - ["pa", _("Pa"), _("Pa"), "x / 100000", "x * 100000"], - ["kpa", _("kPa"), _("kPa"), "x / 100", "x * 100"], - ], + _("Distance"): { + "base": "m", + "units": [ + ("m", _("metre"), _("metres"), 1), + ("dm", _("decimetre"), _("decimetres"), 0.1), + ("cm", _("centimetre"), _("centimetres"), 0.01), + ("mm", _("millimetre"), _("millimetres"), 0.001), + ("µm", _("micrometre"), _("micrometres"), 1e-6), + ("nm", _("nanometre"), _("nanometres"), 1e-9), + ("km", _("kilometre"), _("kilometres"), 1_000), + ("in", _("inch"), _("inches"), 0.0254), # exact + ("ft", _("foot"), _("feet"), 0.3048), # exact + ("yd", _("yard"), _("yards"), 0.9144), # exact + ("mi", _("mile"), _("miles"), 1_609.344), # exact + ("nmi", _("nautical mile"), _("nautical miles"), 1_852), # exact + ("ly", _("light-year"), _("light-years"), 9.4607304725808e15), + ("au", _("astronomical unit"), _("astronomical units"), 1.495978707e11), # exact IAU + ("pc", _("parsec"), _("parsecs"), 3.085677581e16), + ("Å", _("ångström"), _("ångströms"), 1e-10), + ] + }, + + _("Area"): { + "base": "sqm", + "units": [ + ("sqm", _("square metre"), _("square metres"), 1), + ("sqcm", _("square centimetre"), _("square centimetres"), 1e-4), + ("sqmm", _("square millimetre"), _("square millimetres"), 1e-6), + ("sqkm", _("square kilometre"), _("square kilometres"), 1e6), + ("sqin", _("square inch"), _("square inches"), 6.4516e-4), # exact + ("sqft", _("square foot"), _("square feet"), 0.09290304), # exact + ("sqyd", _("square yard"), _("square yards"), 0.83612736), # exact + ("sqmi", _("square mile"), _("square miles"), 2_589_988.110336), + ("ac", _("acre"), _("acres"), 4_046.8564224), + ("h", _("hectare"), _("hectares"), 10_000), + ] + }, + + _("Volume"): { + # Imperial "imp" suffix convention for non-US variants + "base": "ml", + "units": [ + ("ml", _("millilitre"), _("millilitres"), 1), + ("cl", _("centilitre"), _("centilitres"), 10), + ("dl", _("decilitre"), _("decilitres"), 100), + ("l", _("litre"), _("litres"), 1_000), + ("decal", _("decalitre"), _("decalitres"), 10_000), + ("m3", _("cubic metre"), _("cubic metres"), 1_000_000), + ("cm3", _("cubic centimetre"), _("cubic centimetres"), 1), + ("mm3", _("cubic millimetre"), _("cubic millimetres"), 0.001), + ("dm3", _("cubic decimetre"), _("cubic decimetres"), 1_000), + ("in3", _("cubic inch"), _("cubic inches"), 16.387064), # exact + ("ft3", _("cubic foot"), _("cubic feet"), 28_316.846592), + ("pt", _("pint US"), _("pints US"), 473.176473), + ("ptimp", _("pint Imperial"), _("pints Imperial"), 568.26125), + ("qt", _("quart US"), _("quarts US"), 946.352946), + ("qtimp", _("quart Imperial"), _("quarts Imperial"), 1_136.5225), + ("cup", _("cup US"), _("cups US"), 236.588236), + ("cupimp", _("cup Imperial"), _("cups Imperial"), 284.130625), + ("tbsp", _("tablespoon US"), _("tablespoons US"), 14.786765), + ("tbspimp", _("tablespoon Imperial"), _("tablespoons Imperial"), 17.758164), + ("tsp", _("teaspoon US"), _("teaspoons US"), 4.928922), + ("tspimp", _("teaspoon Imperial"), _("teaspoons Imperial"), 5.919388), + ("gal", _("gallon US"), _("gallons US"), 3_785.411784), + ("galimp", _("gallon Imperial"), _("gallons Imperial"), 4_546.09), + ("floz", _("fluid ounce US"), _("fluid ounces US"), 29.573530), + ("flozimp", _("fluid ounce Imperial"),_("fluid ounces Imperial"),28.413063), + ("buuk", _("bushel UK"), _("bushels UK"), 36_368.72), + ("buus", _("bushel US"), _("bushels US"), 35_239.07), + ] + }, + + _("Weight"): { + "base": "g", + "units": [ + ("g", _("gram"), _("grams"), 1), + ("mg", _("milligram"), _("milligrams"), 0.001), + ("µg", _("microgram"), _("micrograms"), 1e-6), + ("ng", _("nanogram"), _("nanograms"), 1e-9), + ("kg", _("kilogram"), _("kilograms"), 1_000), + ("t", _("tonne"), _("tonnes"), 1_000_000), + ("lb", _("pound"), _("pounds"), 453.59237), # exact + ("oz", _("ounce"), _("ounces"), 28.349523125), # exact + ("st", _("stone"), _("stone"), 6_350.29318), + ("ton", _("US ton"), _("US tons"), 907_184.74), + ("tonimp", _("Imperial ton"), _("Imperial tons"), 1_016_046.9088), + ("Da", _("dalton"), _("daltons"), 1.66053906660e-24),# exact (2018 CODATA) + ("u", _("atomic mass unit"), _("atomic mass units"), 1.66053906660e-24), + ] + }, + + _("Temperature"): { + # Affine: to_base = (value + offset) * factor (base = Celsius) + "base": "c", + "units": [ + ("c", _("Celsius"), _("Celsius"), 1, 0), + ("f", _("Fahrenheit"), _("Fahrenheit"), 1/1.8, -32), + ("k", _("Kelvin"), _("Kelvin"), 1, -273.15), + ("r", _("Rankine"), _("Rankine"), 1/1.8, -491.67), + ] + }, + + _("Speed"): { + "base": "m/s", + "units": [ + ("m/s", _("metres per second"), _("metres per second"), 1), + ("km/h", _("kilometres per hour"), _("kilometres per hour"), 1/3.6), + ("mp/h", _("miles per hour"), _("miles per hour"), 0.44704), # exact + ("ft/s", _("feet per second"), _("feet per second"), 0.3048), # exact + ("kt", _("knot"), _("knots"), 0.514444), + ("mach", _("mach"), _("mach"), 340.29), # at sea level 15°C + ("c", _("speed of light"), _("speed of light"), 299_792_458), # exact + ] + }, + + _("Force"): { + "base": "N", + "units": [ + ("N", _("newton"), _("newtons"), 1), + ("mN", _("millinewton"), _("millinewtons"), 0.001), + ("kN", _("kilonewton"), _("kilonewtons"), 1_000), + ("MN", _("meganewton"), _("meganewtons"), 1_000_000), + ("dyn", _("dyne"), _("dynes"), 1e-5), + ("kgf", _("kilogram-force"), _("kilogram-force"), 9.80665), # exact + ("lbf", _("pound-force"), _("pound-force"), 4.4482216152605), + ("pdl", _("poundal"), _("poundals"), 0.138254954376), + ] + }, + + _("Pressure"): { + "base": "Pa", + "units": [ + ("Pa", _("pascal"), _("pascals"), 1), + ("hPa", _("hectopascal"), _("hectopascals"), 100), + ("kPa", _("kilopascal"), _("kilopascals"), 1_000), + ("MPa", _("megapascal"), _("megapascals"), 1_000_000), + ("GPa", _("gigapascal"), _("gigapascals"), 1e9), + ("bar", _("bar"), _("bar"), 100_000), + ("mbar", _("millibar"), _("millibars"), 100), + ("atm", _("atmosphere"), _("atmospheres"), 101_325), # exact + ("mmHg", _("millimetre of mercury"),_("millimetres of mercury"), 133.322387415), + ("torr", _("torr"), _("torr"), 133.322368421), + ("psi", _("psi"), _("psi"), 6_894.757293168), + ("ksi", _("ksi"), _("ksi"), 6_894_757.293168), + ("inHg", _("inch of mercury"), _("inches of mercury"), 3_386.389), + ] + }, + + _("Energy"): { + "base": "J", + "units": [ + ("J", _("joule"), _("joules"), 1), + ("mJ", _("millijoule"), _("millijoules"), 0.001), + ("kJ", _("kilojoule"), _("kilojoules"), 1_000), + ("MJ", _("megajoule"), _("megajoules"), 1_000_000), + ("GJ", _("gigajoule"), _("gigajoules"), 1e9), + ("cal", _("calorie (IT)"), _("calories (IT)"), 4.1868), + ("kcal", _("kilocalorie"), _("kilocalories"), 4_186.8), + ("kWh", _("kilowatt-hour"), _("kilowatt-hours"), 3_600_000), + ("MWh", _("megawatt-hour"), _("megawatt-hours"), 3_600_000_000), + ("BTU", _("British thermal unit"), _("British thermal units"), 1_055.05585262), + ("therm",_("therm"), _("therms"), 105_480_400), + ("eV", _("electronvolt"), _("electronvolts"), 1.602176634e-19),# exact + ("keV", _("kiloelectronvolt"), _("kiloelectronvolts"), 1.602176634e-16), + ("MeV", _("megaelectronvolt"), _("megaelectronvolts"), 1.602176634e-13), + ("GeV", _("gigaelectronvolt"), _("gigaelectronvolts"), 1.602176634e-10), + ("erg", _("erg"), _("ergs"), 1e-7), + ] + }, + + _("Power"): { + "base": "W", + "units": [ + ("W", _("watt"), _("watts"), 1), + ("mW", _("milliwatt"), _("milliwatts"), 0.001), + ("kW", _("kilowatt"), _("kilowatts"), 1_000), + ("MW", _("megawatt"), _("megawatts"), 1_000_000), + ("GW", _("gigawatt"), _("gigawatts"), 1e9), + ("hp", _("horsepower (mech.)"), _("horsepower (mech.)"), 745.69987), + ("hpmet", _("horsepower (metric)"), _("horsepower (metric)"), 735.49875), + ("BTU/hr", _("BTU per hour"), _("BTU per hour"), 0.29307107), + ("kcal/hr", _("kilocalorie per hour"), _("kilocalories per hour"), 1.163), + ("VA", _("volt-ampere"), _("volt-amperes"), 1), + ] + }, + + _("Torque"): { + "base": "Nm", + "units": [ + ("Nm", _("newton-metre"), _("newton-metres"), 1), + ("kNm", _("kilonewton-metre"), _("kilonewton-metres"), 1_000), + ("mNm", _("millinewton-metre"), _("millinewton-metres"), 0.001), + ("lbf_ft", _("pound-foot"), _("pound-feet"), 1.3558179483314), + ("lbf_in", _("pound-inch"), _("pound-inches"), 0.1129848290276), + ("kgfm", _("kilogram-force metre"), _("kilogram-force metres"), 9.80665), + ("ozf_in", _("ounce-force inch"), _("ounce-force inches"), 0.0070615517), + ("dyn_cm", _("dyne-centimetre"), _("dyne-centimetres"), 1e-7), + ] + }, + + _("Frequency"): { + "base": "Hz", + "units": [ + ("Hz", _("hertz"), _("hertz"), 1), + ("kHz", _("kilohertz"), _("kilohertz"), 1_000), + ("MHz", _("megahertz"), _("megahertz"), 1_000_000), + ("GHz", _("gigahertz"), _("gigahertz"), 1e9), + ("THz", _("terahertz"), _("terahertz"), 1e12), + ("rpm", _("RPM"), _("RPM"), 1/60), + ("rps", _("rev/s"), _("rev/s"), 1), + ("rad/s", _("radian/second"), _("radians/second"), 1/(2 * 3.14159265358979323846)), + ] + }, + + _("Angle"): { + "base": "deg", + "units": [ + ("deg", _("degree"), _("degrees"), 1), + ("rad", _("radian"), _("radians"), 180 / 3.14159265358979323846), + ("grad", _("gradian"), _("gradians"), 0.9), + ("arcmin", _("arcminute"), _("arcminutes"), 1/60), + ("arcsec", _("arcsecond"), _("arcseconds"), 1/3600), + ("rev", _("revolution"), _("revolutions"), 360), + ("mrad", _("milliradian"), _("milliradians"), 180 / (3.14159265358979323846 * 1000)), + ] + }, + + _("Amount of substance"): { + "base": "mol", + "units": [ + ("mol", _("mole"), _("moles"), 1), + ("mmol", _("millimole"), _("millimoles"), 0.001), + ("µmol", _("micromole"), _("micromoles"), 1e-6), + ("nmol", _("nanomole"), _("nanomoles"), 1e-9), + ("pmol", _("picomole"), _("picomoles"), 1e-12), + ("kmol", _("kilomole"), _("kilomoles"), 1_000), + ] + }, + + _("Radioactivity"): { + "base": "Bq", + "units": [ + ("Bq", _("becquerel"), _("becquerels"), 1), + ("kBq", _("kilobecquerel"), _("kilobecquerels"), 1_000), + ("MBq", _("megabecquerel"), _("megabecquerels"), 1_000_000), + ("GBq", _("gigabecquerel"), _("gigabecquerels"), 1e9), + ("TBq", _("terabecquerel"), _("terabecquerels"), 1e12), + ("Ci", _("curie"), _("curies"), 3.7e10), + ("mCi", _("millicurie"), _("millicuries"), 3.7e7), + ("µCi", _("microcurie"), _("microcuries"), 37_000), + ("nCi", _("nanocurie"), _("nanocuries"), 37), + ("dpm", _("disintegrations per minute"), _("disintegrations per minute"), 1/60), + ] + }, + + _("Radiation dose"): { + # Absorbed dose — gray (Gy) as base + "base": "Gy", + "units": [ + ("Gy", _("gray"), _("grays"), 1), + ("mGy", _("milligray"), _("milligrays"), 0.001), + ("µGy", _("microgray"), _("micrograys"), 1e-6), + ("cGy", _("centigray"), _("centigrays"), 0.01), + ("rad", _("rad"), _("rads"), 0.01), + ("mrad", _("millirad"), _("millirads"), 1e-5), + ] + }, + + _("Dose equivalent"): { + # Sievert as base + "base": "Sv", + "units": [ + ("Sv", _("sievert"), _("sieverts"), 1), + ("mSv", _("millisievert"),_("millisieverts"), 0.001), + ("µSv", _("microsievert"),_("microsieverts"), 1e-6), + ("rem", _("rem"), _("rem"), 0.01), + ("mrem", _("millirem"), _("millirem"), 1e-5), + ] + }, + + _("Density"): { + "base": "kg/m3", + "units": [ + ("kg/m3", _("kilogram per cubic metre"), _("kilograms per cubic metre"), 1), + ("g/cm3", _("gram per cubic centimetre"),_("grams per cubic centimetre"), 1_000), + ("g/mL", _("gram per millilitre"), _("grams per millilitre"), 1_000), + ("kg/L", _("kilogram per litre"), _("kilograms per litre"), 1_000), + ("g/L", _("gram per litre"), _("grams per litre"), 1), + ("mg/L", _("milligram per litre"), _("milligrams per litre"), 0.001), + ("lb/ft3", _("pound per cubic foot"), _("pounds per cubic foot"), 16.01846337), + ("lb/in3", _("pound per cubic inch"), _("pounds per cubic inch"), 27_679.9047102), + ("lb/gal", _("pound per US gallon"), _("pounds per US gallon"), 119.826427), + ] + }, + + _("Dynamic viscosity"): { + "base": "Pa_s", + "units": [ + ("Pa_s", _("pascal-second"), _("pascal-seconds"), 1), + ("mPa_s", _("millipascal-second"), _("millipascal-seconds"), 0.001), + ("cP", _("centipoise"), _("centipoise"), 0.001), + ("P", _("poise"), _("poise"), 0.1), + ("lb/ft_s",_("pound per foot-second"), _("pound per foot-second"), 1.4881639), + ] + }, + + _("Kinematic viscosity"): { + "base": "m2/s", + "units": [ + ("m2/s", _("square metre per second"), _("square metres per second"), 1), + ("mm2/s", _("square millimetre per second"), _("square millimetres per second"), 1e-6), + ("cSt", _("centistokes"), _("centistokes"), 1e-6), + ("St", _("stokes"), _("stokes"), 1e-4), + ("ft2/s", _("square foot per second"), _("square feet per second"), 0.09290304), + ] + }, + + _("Data"): { + "base": "B", + "units": [ + # SI decimal — bytes + ("B", _("byte"), _("bytes"), 1), + ("KB", _("kilobyte"), _("kilobytes"), 10**3), + ("MB", _("megabyte"), _("megabytes"), 10**6), + ("GB", _("gigabyte"), _("gigabytes"), 10**9), + ("TB", _("terabyte"), _("terabytes"), 10**12), + ("PB", _("petabyte"), _("petabytes"), 10**15), + # SI decimal — bits + ("b", _("bit"), _("bits"), 1/8), + ("Kb", _("kilobit"), _("kilobits"), 10**3 / 8), + ("Mb", _("megabit"), _("megabits"), 10**6 / 8), + ("Gb", _("gigabit"), _("gigabits"), 10**9 / 8), + ("Tb", _("terabit"), _("terabits"), 10**12 / 8), + ("Pb", _("petabit"), _("petabits"), 10**15 / 8), + # IEC binary — bytes + ("KiB", _("kibibyte"), _("kibibytes"), 2**10), + ("MiB", _("mebibyte"), _("mebibytes"), 2**20), + ("GiB", _("gibibyte"), _("gibibytes"), 2**30), + ("TiB", _("tebibyte"), _("tebibytes"), 2**40), + ("PiB", _("pebibyte"), _("pebibytes"), 2**50), + # IEC binary — bits + ("Kib", _("kibibit"), _("kibibits"), 2**10 / 8), + ("Mib", _("mebibit"), _("mebibits"), 2**20 / 8), + ("Gib", _("gibibit"), _("gibibits"), 2**30 / 8), + ("Tib", _("tebibit"), _("tebibits"), 2**40 / 8), + ("Pib", _("pebibit"), _("pebibits"), 2**50 / 8), + ] + }, } + + +# --------------------------------------------------------------------------- +# Conversion engine +# --------------------------------------------------------------------------- + +def convert(value: float, from_abbr: str, to_abbr: str, category: str) -> float: + """Convert value between two units in the same category.""" + unit_map = {u[0]: u for u in units[category]["units"]} + src = unit_map[from_abbr] + dst = unit_map[to_abbr] + + if len(src) == 5: # affine (Temperature, Rankine, etc.) + base_value = (value + src[4]) * src[3] + return (base_value / dst[3]) - dst[4] + else: # linear + return value * src[3] / dst[3]