8

My function is converting a string to Decimal

func getDecimalFromString(_ strValue: String) -> NSDecimalNumber {
    let formatter = NumberFormatter()
    formatter.maximumFractionDigits = 1
    formatter.generatesDecimalNumbers = true
    return formatter.number(from: strValue) as? NSDecimalNumber ?? 0
}

But it is not working as per expectation. Sometimes it's returning like

Optional(8.300000000000001)
Optional(8.199999999999999)

instead of 8.3 or 8.2. In the string, I have value like "8.3" or "8.2" but the converted decimal is not as per my requirements. Any suggestion where I made mistake?

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
Ashwin Kanjariya
  • 1,019
  • 2
  • 10
  • 22

2 Answers2

10

Setting generatesDecimalNumbers to true does not work as one might expect. The returned value is an instance of NSDecimalNumber (which can represent the value 8.3 exactly), but apparently the formatter converts the string to a binary floating number first (and that can not represent 8.3 exactly). Therefore the returned decimal value is only approximately correct.

That has also been reported as a bug:

Note also that (contrary to the documentation), the maximumFractionDigits property has no effect when parsing a string into a number.

There is a simple solution: Use

NSDecimalNumber(string: strValue) // or
NSDecimalNumber(string: strValue, locale: Locale.current)

instead, depending on whether the string is localized or not.

Or with the Swift 3 Decimal type:

Decimal(string: strValue) // or
Decimal(string: strValue, locale: .current)

Example:

if let d = Decimal(string: "8.2") {
    print(d) // 8.2
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Probably it is connected with this answer http://stackoverflow.com/questions/10570756/issues-with-setmaximumfractiondigits ? – Oleg Gordiichuk Apr 21 '17 at 09:33
  • 1
    @OlegGordiichuk: That is related, but the main problem here (as I see it) is that `generatesDecimalNumbers` does not work (which seems to be a bug). – Martin R Apr 21 '17 at 09:35
  • I'm not sure what is the cause of the issue.... out of 100, 99 times its proper but one time its failed. – Ashwin Kanjariya Apr 21 '17 at 10:05
  • @AshwinIndianic: Are you referring to your code or to my suggested solution now? Can you give a concrete example where it fails? – Martin R Apr 21 '17 at 10:12
  • I have tried the answer of "Sunil Pandey". but no success. Anyway thanks for your help. – Ashwin Kanjariya Apr 21 '17 at 10:13
  • @MartinR, Let me try with the locale, As without it it's not working for me. Please give me few minutes – Ashwin Kanjariya Apr 21 '17 at 10:14
  • @MartinR, My core data attribute is like: NSManaged public var personalRating: NSDecimalNumber? Should I change it to Decimal? – Ashwin Kanjariya Apr 21 '17 at 10:19
  • @AshwinIndianic: Decimal is the new Swift 3 overlay type with value semantics. But Core Data still uses NSDecimalNumber for decimal properties, you can keep that as it is. – Martin R Apr 21 '17 at 10:24
  • @MartinR, Yes both of your code is working fine i.e with and without locale, Thanks. I made some fixes for this which is also woking. Anyway changing my code as per your suggestion. (like) – Ashwin Kanjariya Apr 21 '17 at 10:25
  • @AshwinIndianic: If the string comes from *user input* then you probably should use the localized version. There are countries (such as Germany) where a comma is used as decimal separator instead of a period, e.g. `8,2`. If the string always uses a period as separator then you don't need the locale. – Martin R Apr 21 '17 at 10:30
  • @MartinR, Got your point but this is not the case for this string. Thanks for your help – Ashwin Kanjariya Apr 21 '17 at 12:40
  • “`generatesDecimalNumbers` does not work” ... I’m not sure if I’d go quite that far. It does generate `NSDecimalNumber`, but inexplicably does it after (!) first converting the string to a floating point. But you can round the resulting `NSDecimalNumber`... – Rob Feb 28 '21 at 18:43
  • 1
    @Rob: Yes, thanks. What I meant is “does not work as expected” – I have tried to clarify that. – Martin R Feb 28 '21 at 19:23
  • Using `Decimal(string: "156.89")` will still give me "156.78899999999995904".... this does not seem to work. https://i.imgur.com/n34O3PU.png – Tieme Jun 21 '22 at 13:24
  • @Tieme: `Decimal(string: "156.89")` works correctly, but `Decimal(156.89)` does not (it rounds the floating point number *before* putting it into a Decimal) – compare https://stackoverflow.com/q/42781785/1187415. Try this: `print(Decimal(string: "156.89")!) ; print(Decimal(156.89))` – Martin R Jun 21 '22 at 13:35
1

I would probably be inclined to just use Decimal(string:locale:), but if you want to use NumberFormatter, you would just manually round it.

func getDecimalFromString(_ string: String) -> NSDecimalNumber {
    let formatter = NumberFormatter()
    formatter.generatesDecimalNumbers = true
    let value = formatter.number(from: string) as? NSDecimalNumber ?? 0
    return value.rounding(accordingToBehavior: RoundingBehavior(scale: 1))
}

Or if you want to return a Decimal:

func getDecimalFromString(_ string: String) -> Decimal {
    let formatter = NumberFormatter()
    formatter.generatesDecimalNumbers = true
    let value = formatter.number(from: string) as? NSDecimalNumber ?? 0
    return value.rounding(accordingToBehavior: RoundingBehavior(scale: 1)) as Decimal
}

Where

class RoundingBehavior: NSDecimalNumberBehaviors {
    private let _scale: Int16

    init(scale: Int16) {
        _scale = scale
    }

    func roundingMode() -> NSDecimalNumber.RoundingMode {
        .plain
    }

    func scale() -> Int16 {
        _scale
    }

    func exceptionDuringOperation(_ operation: Selector, error: NSDecimalNumber.CalculationError, leftOperand: NSDecimalNumber, rightOperand: NSDecimalNumber?) -> NSDecimalNumber? {
        .notANumber
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044