Rounding Float to two decimal places

I have a float value that detriments by 1.0, but the output can be 59.999, or 69.99998 at times and so I don't get round numbers like 60, 70, 80...

I have a workaround, but I would like to know if there is a more elegant way:

    func setYield(total: Float) {
        
        let currentYieldExample = 10
        var totalYield = currentYieldExample + totalYield
        let numberOfPlaces = 2.0
        let multiplier = pow(10.0, numberOfPlaces)
        let rounded = round(Double(totalYield) * multiplier) / multiplier
        
        print("\(Int(rounded * 100))%")
    }

I got the workaround from How to round a Double to the nearest Int in swift? - Stack Overflow, but fitting it into my existing code is a little sloppy.

NumberFormatter is something perfect for that kind of thing!

import Foundation

var formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
let value = 59.999
if let formattedString = formatter.string(for: value) {
    print(formattedString, "%")
}
2 Likes

Many thanks, NumberFormatter was key!

I ended up with the following code:

import Foundation

var formatter = NumberFormatter()
formatter.maximumFractionDigits = 0
formatter.minimumFractionDigits = 0
formatter.numberStyle = .percent

let value = 59.999
if let formattedString = formatter.string(for: value) {
    print(formattedString)
}

//outputs: "60%"

Thanks for the help!

1 Like

The NumberFormatter does not fit when you just want a rounded Double or Float again as the output. I don't know why these basic number operations are not implemented for these types.

I would expect rounding to give you a round number... but that's often impossible with floats

2 Likes

I believe the Numerics package has facilities for these. I'm no maths guy, but from following threads over the years, I have come to the understanding that there's no simple answer for what it means to round.

A Double or Float or any BinaryFloatingPoint type cannot have a value that is rounded to some (non-zero) number of decimal places in general, because almost all such values are not representable in any binary floating-point type. Because of this, the operation only makes sense as a formatting operation, which converts to a decimal string.

This is an often-requested misfeature; users think they want it, and many languages and libraries provide it, but it is a frequent source of subtle bugs.

You can scale by a multiple of 10 and round to an integer, then keep track of that scale. This is sometimes a desirable solution. For example, rather than rounding to two decimal places, you can multiply by 100 and round to an integer. This does not have the representation problems that I described previously, but you have to keep track of the scale factor.

3 Likes

... almost all such values are not representable in any binary floating-point type

I have to dig deeper and learn why is the sentence above true.

You can scale by a multiple of 10 and round to an integer, then keep track of that scale.

That's exactly what I do in my codebase now. But it doesn't feel ergonomic and right. But as I said - I have to learn more about the problem itself.

Thank you for this explanation :+1:

You don't need to know anything about floating-point to understand why, just a little bit about different bases. Every integer is exactly representable by a finite digit string in any integer base, but that isn't true for fractions. E.g. consider the base 3 string "0.1", or 1/3. Every kid learns that in decimal this is an infinitely repeating string: "0.333333...". If I asked you to "round 4/10 to one base-three digit" as a decimal number, you couldn't do it, because the result should be 1/3, which isn't representable in base ten. Exactly the same thing is happening here.

2 Likes

So when we can "get around" the representation problems with the multiplication by 10, rounding to an integer, tracking the scale factor etc. - why it can't be implemented in this way in Swift itself? You said that other languages implement rounding for various kinds of numerics.

Regardless of Float to String conversion we could imagine a function that rounds a float to a particular number of digits. e.g.:

func round(_ v: Double, radix: Int = 10, places: Int) -> Double {
    let factor = pow(Double(radix), Double(places))
    return (v * factor).rounded() / factor
}

round(0.1234567, places: 3)

could return smth like 0.12300000001 or 0.1229999999997 (when converted to string).

Yes, they get it wrong and introduce bugs into people’s programs.

swift's lack of a standard Decimal type is really coming through in this thread

1 Like

Eh, yes and no. Even when you have decimal, people still ask for this operation on float and double, and most of the time people ask for it on decimal, they shouldn’t use it there either (except in rare circumstances, rounding should be a display operation rather than a computational operation, because subsequent operations on the rounded result are undesirable most of the time).

2 Likes

i’m not sure i understand. most of the time decimal rounding is mandatory, since otherwise the number of decimal places accumulates with every multiplication operation. and when performing divisions it’s basically unavoidable.

We're not talking about normal arithmetic rounding here, we're talking about the specific operation of "round a number to a prescribed number of decimal places (e.g. 2)" as its own operation. It is undesirable to do this except for display purposes or when explicitly required by the terms of a financial calculation, even when working in decimal arithmetic (because the rounding errors introduced by doing so propagate through and are magnified by all further computations, you should always keep the most accurate value you have, rather than rounding to a nice number if you have a choice).

1 Like

right, i was just about to say this is quite common in financial applications. oftentimes this precision is business-defined and completely unrelated to the arithmetic. a BTC balance might be stored with 8 decimal places, but only supports transfers in increments of 0.0001 and lending in increments of 0.01. it’s pretty common for spot lending to use larger increments than an asset is normally denominated in, since it would gain many more decimal places when multiplied with an (hourly) interest rate.

a possible use case might be “user wants to lend 50% of her current balance, rounded to _ decimal places”.