I am trying to convert a float number to an integer. The reason is that I want the E8S equivalent of 1 ICP of a value, for example 1.15. This should be 115_000_000, but the function returns 114_000_000.

Other values are calculated correctly, for example 1.25 = 125_000_000 or 0.9 = 90_000_000.

public func convertFloatToNat(x: Float): async Int {
let ONE_ICP_IN_E8S = 100_000_000;
Debug.print("convertFloatToNat "#debug_show(x));
let a = x * 100;
Debug.print("float x 100 = "#debug_show(a));
let b:Int = Float.toInt(a);
Debug.print("float to int = "#debug_show(b));
let c = (ONE_ICP_IN_E8S * b) / 100;
return c;
}

The representation of Float in the canister is adhering to the IEEE format, which is a non-exact way of doing reals on a computer. It is generally discouraged of performing monetary calculations in floating-point form exactly for this reason. Better use the Int (or Nat when positive amounts) for those.

I just thought for interest, to add some details: It is a combination of a numerical error and the conversion by truncation:

The computation 1.15 * 100 gets represented as 114.99999999999999 due to the usual numeric erros of float (see Gabor’s comment). When displaying the value, however, with debug_show, it gets rounded back to 115.00000.

Then Float.toInt truncates the decimal places (cf. the documentation). So, 114.99999999999999 results in 114.

@ggreif your modified code produces the same wrong result as my code with the x 100 trick. You can check it in the Playground.

@luc-blaeser This is actually a behavior that I have also seen in languages.

But that doesn’t solve my problem. I’m trying to implement a simple invoicing solution where you can pay with ICP. It’s not unlikely to have a selling price of 1.15 ICP. For this reason I have to work with float numbers.

If the calculation is not possible exactly, do I need to delegate this calculation to the JavaScript client. Or is there another solution to fix this problem?

I believe the best solution would be to pass in the ICP value as Nat, however with some multiplier for the decimal places (e.g. in cents), e.g. 115 for actual 1.15. With that, all operations in Motoko work on decimal numbers and one can avoid the floating point numeric errors. The downside is that it would need some changes in the frontend to do the multiplication there upfront.

Btw: The number type in JavaScript is also only a floating point number type, such that the problem of numeric errors exists there too.

FWIW, the general advise not to use floats for monetary computations is universal and language-independent. You can find many explanations for that online, e.g. on StackOverflow.

Ugh, I have just checked, 1.15 ICP returns 114999999i8s which is not too far off the 115000000 one would expect. The discrepancy is due to the inexactness of the floating-point representation.