0

I am using String(format:) to convert a Float. I thought the number would be rounded.

Sometimes it is.

String(format: "%.02f", 1.455)     //"1.46"

Sometimes not.

String(format: "%.02f", 1.555)     //"1.55"
String(round(1.555 * 100) / 100.0) //"1.56"

enter image description here

I guess 1.55 cannot be represented exactly as binary. And that it becomes something like 1.549999XXXX

But NumberFormatter doesn't seem to cause the same problem... Why? Should it be preferred over String(format:)?

let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2

if let string = formatter.string(for: 1.555) {
    print(string)  // 1.56
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
Adrien
  • 1,579
  • 6
  • 25
  • 1
    Yes you should always favor `NumberFormatter`. It also gives you an option to choose the rounding mode. https://stackoverflow.com/a/27705739/2303865. Btw if you want to keep the fraction digits precision you should use Decimal type and its string initializer – Leo Dabus Aug 16 '21 at 00:38
  • 1
    Re why we use `Decimal`, see [What every computer scientist should know about floating point arithmetic](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html). – Rob Aug 16 '21 at 04:13
  • Thank you for your answers. I understand that it is not a good idea to use a Float if you hope to accurately represent a decimal number (maybe an [IEEE-754 Float Converter](https://www.h-schmidt.net/FloatConverter/IEEE754.html) is too a good way to figure it out). – Adrien Aug 16 '21 at 11:02
  • @Rob : But my question was about `String(format:)`. In many (highly upvoted) answers to questions about "rounding a decimal number" we find the proposition `String(format:)`. Let's take just one example, [one of the best answers on this point](https://stackoverflow.com/a/38036978/16125496). It says "NSString initializer is a simple but efficient solution". So, should we now say that this is a bad solution and that NumberFormatter must always be favored? Does it never make rounding errors? Would it make sense to do this: `String(format: "% .02f", NSDecimalNumber(1.555).doubleValue)`? – Adrien Aug 16 '21 at 11:05
  • My point is simply that one can never understand any rounding algorithm (whether number formatter or `String(format:)`) until one realizes that binary floating point representations (e.g., `Double`) are simply unable to precisely represent most fractional decimal numbers. If you’re worried about the two decimal place `String(format: "%0.02f", 1.455)`, just wrap your head around the 30 decimal place rendition, `String(format: "%0.30f", 1.455)`. Essential to understanding weird rounding behaviors is to grok the mind-bending reality that a `Double` representation of 1.455 is not actually 1.455. – Rob Aug 16 '21 at 13:20
  • Re Leo’s recommendation of `NumberFormatter`, I agree with him. The fact that there are old answers advocating `NSString` doesn’t mean it’s right. It only means that there are lots of devs who are shocking oblivious to international users. Sure `NumberFormatter` has slightly more refined rounding behaviors (which is true, though immaterial), but more importantly, it supports international users (e.g. in Germany, 1.455 rounded to two decimal places is “1,46”, not “1.46”). In general, when displaying values in the UI, use formatters. https://developer.apple.com/videos/play/wwdc2020/10160/ – Rob Aug 16 '21 at 13:50
  • 1
    @Rob : Understood. I was confusing (as in a lot of the answers on this topic, I feel like) doing math and formatting a number. It is (as often) a question of use. Usually we want to round a number before displaying it, because we believe that the user is not interested in a high level of precision. In this case the rare weird rounding behaviors are negligible and the localization issue is certainly more important, so we use a `NumberFormatter`. If precision really matters, and we're doing calculations, then we're probably already using `Decimal` or simple integers. Thank you. – Adrien Aug 16 '21 at 18:22

1 Answers1

2

Reference to the problem (to use String (format :) to round a decimal number) can be found in the answers (or more often comments) to these questions: Rounding a double value to x number of decimal places in swift and How to format a Double into Currency - Swift 3. But the problem it covers (math with FloatingPoint) has been dealt with many times on SO (for all languages).

String(format:) does not have the function of rounding a decimal number (even if it is unfortunately proposed in some answers) but of formatting it (as its name suggests). This formatting sometimes causes a rounding. That is true. But we have to keep in mind a problem that the number 1.555 is... not worth 1.555.

In Swift, Double and Float, that conform to the FloatingPoint protocol respect the IEEE 754 specification. However, some values ​​cannot be exactly represented by the IEEE 754 standard.

In the same way that you can't represent a third exactly in a (finite) decimal expansion, there are lots of numbers which look simple in decimal, but which have long or infinite expansions in a binary expansion." (source)

To be convinced of this, we can use The Float Converter to convert between the decimal representation of numbers (like "1.02") and the binary format used by all modern CPUs (IEEE 754 floating point). For 1.555, the value actually stored in float is 1.55499994754791259765625

So the problem does not come from String (format :). For example, we can try another way to round to the thousandth and we find the same problem. :

round (8.45 * pow (10.0, 3.0)) / pow (10.0, 3.0)
// 8.449999999999999

That is how it is : "Binary floating point arithmetic is fine so long as you know what's going on and don't expect values ​​to be exactly the decimal ones you put in your program".

So the real question is : is this really a problem for you to use ? It depends on the app. Generally if we convert a number into a String by limiting its precision (by rounding), it is because we consider that this precision is not useful to the user. If this is the kind of data we're talking about, then it's okay to use a FloatingPoint.

However, to format it it may be more relevant to use a NumberFormatter. Not necessarily for its rounding algorithm, but rather because it allows you to locate the format :

let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
formatter.locale = Locale(identifier: "fr_FR")
formatter.string(for: 1.55)!
// 1,55
formatter.locale = Locale(identifier: "en_US")
formatter.string(for: 1.55)!
// 1.55

Conversely, if we are in a case where precision matters, we must abandon Double / Float and use Decimal. Still to keep our rounding example, we can use this extension (which may be the best answer to the question "Rounding a double value to x number of decimal places in swift ") :

extension Double {
    func roundedDecimal(to scale: Int = 0, mode: NSDecimalNumber.RoundingMode = .plain) -> Decimal {
        var decimalValue = Decimal(self)
        var result = Decimal()
        NSDecimalRound(&result, &decimalValue, scale, mode)
        return result
    }
}

1.555.roundedDecimal(to: 2)
// 1.56
Adrien
  • 1,579
  • 6
  • 25