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, "%")
}
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%"
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 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.
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.
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.
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).
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).
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”.