iosswiftnsnumberformatterswift5

NSNumberFormatter not working as expected


We trying to do some operation with Rounding the values after X number of decimals. To do that We are using NSNumberFormatter class. The results comes wrong.

Working JAVA Code

public static double roundOff(double value) {
        DecimalFormat df2 = new DecimalFormat("#.##########");
        df2.setRoundingMode(RoundingMode.HALF_EVEN);
        return Double.parseDouble(df2.format(value));
    }
    
    public static double roundOff2(double value) {
        DecimalFormat df2 = new DecimalFormat("#.###");
        df2.setRoundingMode(RoundingMode.HALF_EVEN);
        return Double.parseDouble(df2.format(value));
    }
    public static double roundOff2Step(double value) {
        DecimalFormat df2 = new DecimalFormat("#.###");
        df2.setRoundingMode(RoundingMode.HALF_EVEN);
        return Double.parseDouble(df2.format(value));
    }

The Swift Code We tried.

public func convertToRoundValue(number : Double)->Double{
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    formatter.maximumFractionDigits = 2
    formatter.roundingMode = .halfEven
    let str = String(describing: formatter.string(from: NSNumber(value:number))!)
    return str.toDouble()!
}

Is it possible to convert java code to swift? This calculations are purely based on Banking Sectors.

Example values :

8.60455187289436 to 8.61 In java code

8.60455187289436 to 8.60 In Swift code

My assumption :

//8.60455187289436
//8.60455187289444
//8.6045518728944
//8.604551872894
//8.60455187289
//8.6045518729
//8.604551873
//8.60455187
//8.6045519
//8.604552
//8.60455
//8.6046
//8.605
//8.61
//8.6

Thanks to All


Solution

  • A few observations:

    1. Don't use number formatters for this conversion. That's very inefficient. Use the round or rounded functions. And if you want to round to 2 decimal places, you would multiple by 100, round, and then divide by 100.

      E.g., to round a Double value to a certain number of decimal places using a banker’s rounding, it would be:

      extension Double {
          func rounded(to decimalPlaces: Int) -> Double {
              let multiplier = pow(10, Double(decimalPlaces))
      
              return (self * multiplier).rounded(.toNearestOrEven) / multiplier
          }
      }
      

      Thus:

      let value: Double = 8.60455187289436
      let result = value.rounded(to: 2)       // 8.60
      
    2. The use of banker’s rounding makes me wonder if this is a financial calculation. If so, one should acknowledge problems where many decimal values cannot be represented accurately in floating point types. See What Every Computer Scientist Should Know About Floating-Point Arithmetic.

      My favorite manifestation of this problem is adding 0.1 ten times. With floating point types, this will not work:

      var sum: Double = 0
      let increment: Double = 0.1
      
      for _ in 0 ..< 10 {
          sum += increment
      }
      
      if sum != 1 {
          print("This is \(sum)!!!")                // This is 0.9999999999999999!!!
      }
      

      You can use Decimal to avoid that problem:

      var sum: Decimal = 0
      let increment = Decimal(sign: .plus, exponent: -1, significand: 1)  // or Decimal(string: "0.1")!
      
      for _ in 0 ..< 10 {
          sum += increment
      }
      
      if sum == 1 {
          print("Total is 1")
      }
      

      Bottom line, if dealing with currency values, you might consider using Decimal rather than Double.

    3. So, if you want to round using Decimal you might use:

      extension Decimal {
          func rounded(to decimalPlaces: Int, rounding mode: NSDecimalNumber.RoundingMode = .bankers) -> Decimal {
              var input = self
              var result = Decimal()
              NSDecimalRound(&result, &input, decimalPlaces, mode)
              return result
          }
      }
      

      And, again:

      let value: Decimal = 8.60455187289436
      let result = value.rounded(to: 2)            // 8.60
      
    4. You gave the example where 8.60455187289436 was rounded to 8.61 in Java using these routines, using HALF_EVEN. I don't believe that’s the case. For example, with:

      public double roundOff(double value) {
          DecimalFormat df = new DecimalFormat("#.##");
          df.setRoundingMode(RoundingMode.HALF_EVEN);
          return Double.parseDouble(df.format(value));
      }
      

      I got these results:

      double result1 = roundOff(8.60455187289436); // 8.60
      double result2 = roundOff(8.605);            // 8.60
      double result3 = roundOff(8.615);            // 8.62
      
      Log.d("RoundingDebug", Double.toString(result1) + " " + Double.toString(result2) + " " + Double.toString(result3));
      

      If you're getting different results, please show us code. But whether using banker’s rounding or just plain rounding, 8.60455187289436 will never produce 8.61.

    5. All of the discussion presumes that you really need to round the results (e.g. for saving in some data store, the basis for future calculations, etc.). If, however, your intent was simply to show the value to a certain number of decimal places, then a formatter is sufficient. For example, you might define your formatter as a property:

      let formatter: NumberFormatter = {
          let formatter = NumberFormatter()
          formatter.numberStyle = .decimal
          formatter.minimumFractionDigits = 2
          formatter.maximumFractionDigits = 2
          formatter.roundingMode = .halfEven
          return formatter
      }()
      

      But one would use this formatter solely for the sake of displaying the number as a localized string in the UI to a certain number of decimal places (or for parsing user input). But one would not use this formatter for round-tripping a Double to a String and back in order to perform rounding. That is an inefficient way of performing rounding calculations.