17

I want to count the number of decimal places (ignoring trailing zeros) in a Float (or NSDecimalNumber) for example:

1.45000 => 2
5.98 => 2
1.00 => 0
0.857 => 3
5 => 0

How can I accomplish this?

Kashif
  • 4,642
  • 7
  • 44
  • 97
  • 7
    That does not make much sense with binary floating point numbers because they cannot represent all values precisely. For example, with `let x = 1.45` the variable `x` actually holds the value `1.4499999999999999555910790149937383830547332763671875`. – Martin R Jan 19 '17 at 14:33
  • Have a look at http://stackoverflow.com/questions/588004/is-floating-point-math-broken and http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html. – Martin R Jan 19 '17 at 14:36

5 Answers5

26

Doing this with Decimal is fairly straightforward, provided you correctly create your Decimal. Decimals are stored as significand * 10^exponent. significand is normalized to the smallest integer possible. So for 1230, the significand is 123 and the exponent is 1. For 1.23 the significand is also 123 and the exponent is -2. That leads us to:

extension Decimal {
    var significantFractionalDecimalDigits: Int {
        return max(-exponent, 0)
    }
}

However, you must be very careful constructing your Decimal. If you construct it from a Double, you will already have applied binary rounding errors. So for example:

let n = Decimal(0.111) // 0.11100000000000002048 because you passed a Double
n.significantFractionalDecimalDigits // 20

vs.

let n = Decimal(string: "0.111")!
n.significantFractionalDecimalDigits // 3 what you meant

Keep in mind of course that Decimal has a maximum number of significant digits, so it may still apply rounding.

let n = Decimal(string: "12345678901235678901234567890.1234567890123456789")!
n.significantFractionalDecimalDigits // 9 ("should" be 19)

And if you're walking down this road at all, you really must read the Floating Point Guide and the canonical StackOverflow question: Is floating point math broken?

Monica Granbois
  • 6,632
  • 1
  • 17
  • 17
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
11

For me, there is quite easy solution that works on any region and device because iOS will handle possible binary errors for you automatically:

extension Double {
    func decimalCount() -> Int {
        if self == Double(Int(self)) {
            return 0
        }

        let integerString = String(Int(self))
        let doubleString = String(Double(self))
        let decimalCount = doubleString.count - integerString.count - 1

        return decimalCount
    }
}

Edit: it should work same for Double or Float

Ivo Leko
  • 720
  • 1
  • 10
  • 20
  • 2
    Great answer. This may fail though if `String(Double(self))` prints the number using scientific notation (e.g.: "1.2e-6"). To make sure one gets Double as decimal value only, use `"\(NSNumber(value: Double(self)).decimalValue)"`. – backslash-f Feb 11 '22 at 20:56
3

This is terrible , but you can actually count the length of the string after the dot.

extension Double {
    
    var decimalPlaces: Int {
        let decimals = String(self).split(separator: ".")[1]
        return decimals == "0" ? 0 : decimals.count
    }
}
print(3.decimalPlaces) // 0
print(3.1.decimalPlaces) // 1
print(3.14.decimalPlaces) // 2
print(3.1415.decimalPlaces) // 4
print(3.141592.decimalPlaces) // 6
print(3.14159265.decimalPlaces) // 8
Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
  • 5
    This is not safe method, because number can be formatted to "3.14" or "3,14", depending of region of device. – Ivo Leko Jan 15 '21 at 19:55
  • @IvoLeko **Actually I don't disagree at all.** I would not use it in production either, yet it did the job nicely for some counting tasks in an internal/prototype project. I like how it leverages the decimal calculation thing implemented in the `String` type itself. – Geri Borbás Jan 15 '21 at 19:59
1

This is actually really hard due to floating point not being representable precisely in a decimal format. For example the nearest 64 bit IEEE754 floating point to 5.98 is

5.980000000000000426325641456060111522674560546875

Presumably in this case you want the answer to be 2.

The easiest thing to do is to use your favourite converter to a string, formatted to 15 significant figures (for a double precision type) and inspect the output. It's not particularly fast, but it will be reliable. For a 32 bit floating point type, use 7 significant figures.

That said, if you can use a decimal type from the get-go then do that.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • 1
    I like the idea of decimal type, I can change the data type. But how do I count the precision on NSDecimalNumber then? – Kashif Jan 19 '17 at 14:53
0

What about this approach? According to Here both Float and Double are BinaryFloatingPoint. So:

public extension Numeric where Self: BinaryFloatingPoint {

    /// Returns the number of decimals. It will be always greater than 0
    var numberOfDecimals: Int {
        let integerString = String(Int(self))
        //Avoid conversion issue
        let stringNumber: String
        if self is Double {
            stringNumber = String(Double(self))
        }
        else {
            stringNumber = String(Float(self))
        }
        let decimalCount = stringNumber.count - integerString.count - 1

        return decimalCount
    }
    
}
Reimond Hill
  • 4,278
  • 40
  • 52