I Don't Tell
I Don't Tell

Reputation: 323

How to properly reduce scale and format a Float value in Swift?

I'm trying to properly reduce scale, formatting a float value and returning it as a String in Swift.

For example:

let value: Float = 4.8962965

// formattedFalue should be 4.90 or 4,90 based on localization
let formattedValue = value.formatNumber()

Here is what I did:

extension Float {

func reduceScale(to places: Int) -> Float {
      let multiplier = pow(10, Float(places))
      let newDecimal = multiplier * self // move the decimal right
      let truncated = Float(Int(newDecimal)) // drop the fraction
      let originalDecimal = truncated / multiplier // move the decimal back                 return originalDecimal
 }

func formatNumber() -> String {
      let num = abs(self)

      let numberFormatter = NumberFormatter()
      numberFormatter.usesGroupingSeparator = true
      numberFormatter.minimumFractionDigits = 0
      numberFormatter.maximumFractionDigits = 2
      numberFormatter.roundingMode = .up
      numberFormatter.numberStyle = .decimal
      numberFormatter.locale = // we take it from app settings 

      let formatted = num.reduceScale(to: 2)
      let returningString = numberFormatter.string(from: NSNumber(value: formatted))!
      return "\(returningString)"

}

}

But when I use this code I get 4.89 (or 4,89 depending on the localization) instead of 4.90 (or 4,90) as I expect.

Thanks in advance.

Upvotes: 0

Views: 238

Answers (3)

user652038
user652038

Reputation:

Foundation has a better API now.

In-place, you can use .number:

value.formatted(.number
  .precision(.fractionLength(2))
  .locale(locale)
)

But it's only available on specific types. For an extension for more than one floating-point type, you'll need to use the equivalent initializer instead:

extension BinaryFloatingPoint {
  var formattedTo2Places: String {
    formatted(FloatingPointFormatStyle()
      .precision(.fractionLength(2))
      .locale(locale)
    )
  }
}
let locale = Locale(identifier: "it")
(4.8962965 as Float).formattedTo2Places // 4,90

Upvotes: 0

I Don't Tell
I Don't Tell

Reputation: 323

Solved by following Sulthan's comments:

remove that reduceScale method which is not necessary and it will probably work as expected. You are truncating the decimal to 4.89 which cannot be rounded any more (it is already rounded). – Sulthan 6 hours ago

That's because you have specified minimumFractionDigits = 0. If you always want to display two decimal digits, you will have to set minimumFractionDigits = 2. – Sulthan 5 hours ago

Upvotes: 1

JeremyP
JeremyP

Reputation: 86661

You get 4.89 because reduceScale(to:) turns the number into 4.89 (actually, probably 4.89000something because 4.89 cannot be expressed exactly as a binary floating point). When the number formatter truncates this to two decimal places, it naturally rounds it down.

In fact, you don't need reduceScale(to:) at all because the rounding function of the number formatter will do it for you.

Also the final string interpolation is unnecessary because the result of NumberFormatter.string(from:) is automatically bridged to a String?

Also (see comments below by Dávid Pásztor and Sulthan) you can use string(for:) to obviate the NSNumber conversion.

This is what you need

import Foundation

extension Float {

    func formatNumber() -> String {
          let num = abs(self)

          let numberFormatter = NumberFormatter()
          numberFormatter.usesGroupingSeparator = true
          numberFormatter.minimumFractionDigits = 0
          numberFormatter.maximumFractionDigits = 2
          numberFormatter.roundingMode = .up
          numberFormatter.numberStyle = .decimal
          numberFormatter.locale = whatever
          return numberFormatter.string(for: num)!
    }
}

let value: Float = 4.8962965

// formattedFalue should be 4.90 or 4,90 based on localization
let formattedValue = value.formatNumber() // "4.9"

Upvotes: 2

Related Questions