How to solve floating number issue in GNU units-like app?

Hello Python Discourse, First of all, thank you for reading this. I hope you have a nice day. I have a question, I want to make miniature calculator app like GNU units but I am stumbling with floating-point number precision (if this word correct to describe my problem :wink: ), This the code I make;

import sys

length = {
    "yottameter": 1e24,
    "zettameter": 1e21,
    "exameter": 1e18,
    "petameter": 1e15,
    "terameter": 1e12,
    "gigameter": 1e9,
    "megameter": 1e6,
    "kilometer": 1e3,
    "hectometer": 1e2,
    "decameter": 1e1,
    "meter": 1e-0,
    "decimeter": 1e-1,
    "centimeter": 1e-2,
    "millimeter": 1e-3,
    "micrometer": 1e-6,
    "nanometer": 1e-9,
    "picometer": 1e-12,
    "femtometer": 1e-15,
    "attometer": 1e-18,
    "zeptometer": 1e-21,
    "yoctometer": 1e-24,
}


def split_args(input_args) -> list:
    list_input = input_args.split(" ", 1)
    return list_input


def check_input(input: str):
    args = split_args(input)
    if args[1] in length.keys():
        pass
    else:
        print("unit not implemented yet!")
        sys.exit()


def check_input_wanted(input: str):
    if input in length.keys():
        pass
    else:
        print("unit not implemented yet!")
        sys.exit()


def convert_to_primitive(unit: list) -> float:
    primitive_value = length.get(unit[1]) * float(unit[0])
    return primitive_value


def convert_to_wanted_value(primitive_value: float, wanted_result: str) -> float:
    result = primitive_value / length.get(wanted_result)
    return result


def main():
    input_questions: str = input("You have = ")
    input_wanted_result: str = input("You want = ")
    check_input(input_questions)
    check_input_wanted(input_wanted_result)
    input_value = split_args(input_questions)
    if input_wanted_result != "meter":
        convert = convert_to_primitive(input_value)
        result = convert_to_wanted_value(convert, input_wanted_result)
        print(result, input_wanted_result)
    elif input_wanted_result == "meter":
        convert = convert_to_primitive(input_value)
        print(convert, input_wanted_result)
    else:
        print("Error")


if __name__ == "__main__":
    main()

when the input is not too big it’s okay:

❯ python .\pyunits\pyunits.py
You have = 100 meter
You want = kilometer
0.1 kilometer

But when the input is too big the answer will be like this:

❯ python .\pyunits\pyunits.py
You have = 1 meter
You want = nanometer
999999999.9999999 nanometer

How do i solve this problem, any pointer will be appreciated.

Not sure if this helps, but if you limit the number of decimal places, you could do this:

meter = 1

nanometer = 1e-9

output = meter / nanometer

print(output)

output2="{:.6f}".format(output)

print(output2)

or…
print(round(output, 6))

By Gunung P. Wibisono via Discussions on Python.org at 15May2022 07:27:

Hello Python Discourse, First of all, thank you for reading this. I
hope you have a nice day. I have a question, I want to make miniature
calculator app like GNU units
but I am stumbling with floating-point number precision (if this word
correct to describe my problem :wink: ), This the code I make;

import sys

length = {
   "yottameter": 1e24,
   "zettameter": 1e21,
   "exameter": 1e18,
   "petameter": 1e15,
   "terameter": 1e12,
   "gigameter": 1e9,
   "megameter": 1e6,
   "kilometer": 1e3,
   "hectometer": 1e2,
   "decameter": 1e1,
   "meter": 1e-0,
   "decimeter": 1e-1,
   "centimeter": 1e-2,
   "millimeter": 1e-3,
   "micrometer": 1e-6,
   "nanometer": 1e-9,
   "picometer": 1e-12,
   "femtometer": 1e-15,
   "attometer": 1e-18,
   "zeptometer": 1e-21,
   "yoctometer": 1e-24,
}

[…]

def convert_to_primitive(unit: list) → float:
primitive_value = length.get(unit[1]) * float(unit[0])
return primitive_value
def convert_to_wanted_value(primitive_value: float, wanted_result: str) → float:
result = primitive_value / length.get(wanted_result)
return result
[…]
if input_wanted_result != “meter”:
convert = convert_to_primitive(input_value)
result = convert_to_wanted_value(convert, input_wanted_result)
print(result, input_wanted_result)
[…]
❯ python .\pyunits\pyunits.py
You have = 1 meter
You want = nanometer
999999999.9999999 nanometer

This is because floating point is not a precise expression of fractional
decimal values. The underlying representation is base 2, and therefore
dividing by a multiple of 10, which is the product of 2*5, is inherently
inaccurate. As you can see above, not very inaccurate because floats
have quite a lot of precision; however that precision is finite.

You could do a lot of fiddly mucking about to avoid division, but even
then you’d be at the mercy of the finite resolution of a float - it has
a fixed number of significant digits.

Instead, have a look at the decimal module:
https://docs.python.org/3/library/decimal.html#module-decimal

It exists expressly to preform base 10 arithmetic, and should do what
you need. Make sure you set the precision to sufficient digits - its
default is 28 digits, but your scale looks like it needs at least 49
digits, plus a bit more to accomodate whatever value the user might
have.

Cheers,
Cameron Simpson cs@cskk.id.au