Barbiyong
Barbiyong

Reputation: 147

How to convert double to string with no zero absent in kotlin?

I have two Double value but different decimal places.

123.400 // three decimal places
567.80 // two decimal places

I try to convert Double value to String by using DecimalFormat

val stringFormat = DecimalFormat("#,###.000")
println(stringFormat.format(123.400))
println(stringFormat.format(567.80))
    
val stringFormatZeroAbsent = DecimalFormat("#,###.###")
println(stringFormatZeroAbsent.format(123.400))
println(stringFormatZeroAbsent.format(567.80))

Since "0" mean Digit and "#" mean Digit with zero shows as absent, so outputs are:

123.400
567.800
123.4
567.8

The actual result i want are:

123.400
567.80

That the zero not absent.

Upvotes: 0

Views: 2581

Answers (3)

rzwitserloot
rzwitserloot

Reputation: 102814

I have two Double value but different decimal places.

Doubles don't store that information. In fact, they don't even store decimals. Computers are binary after all. Here's a secret:

double d = 0.1;

That line, is a total lie. It does not store 0.1 in d. That is because it is impossible to store this number.

Before that makes you go: What.. the? Think about it: I divide 1 by 3 and ask you to write that down on a piece of paper. So, you start, 0.33333333.... and where do you end? See? As a human using decimal, you cannot store this number on a piece of paper in decimal format.

Computers don't do decimal, they do binary. They can store 0.5 perfectly. 0.25 is no problem either. But 0.1? No, no can do. a tenth works out in decimal really nice, but in binary it doesn't (nothing does, except factors of 2, obviously). So, where in decimal you can write '1/10' on a piece of paper perfectly, but you cannot write '1/3' on a piece of paper perfectly, computers can only write X/Y where Y is a factor of 2 perfectly. 1/65536 - no problem. 1/10? Nope.

So why does the above compile? Because double/float math will round to the nearest representable number, and each mathematical operation is similarly rounded. Everything rounds, everywhere, at all times, silently, with no way to know how much error this rounding introduces.

Let's see it in action!

double d = 0;
for (int i = 0; i < 8; i++) d += 0.1;
double e = 0.8;
System.out.println(d == e); // prints... f... false? What?
System.out.println(e); // prints "0.8"
System.out.println(d); // prints "0.7999999999999999"

Thus, when talking about 'formatting' a double in the sense of 'I want x digits', this is completely misleading, because the thing that is in that variable has digits written out in binary and not in decimal, and is rounded to something that doesn't even make sense in decimal.

The simple upshot is that all doubles are slightly wrong and if you are writing systems or applications where this slight error is not acceptable (trivial example: Everything financial - you do NOT want to lose a random cent due to impossible to predict rounding errors!!), then you cannot use double.

There are 2 broad solutions to this problem:

  1. Use atoms, in int or long.

For example, do not store an item that costs a buck and a half as double price = 1.50. Store it as int price = 150; - in other words, find the atomary (smallest hard/impossible to divide unit) for whatever it is you are attempting to store perfectly and store that. Given that there is such a thing as an atomary, when asked to divide you're hosed anyway and you need to think about it, there is no one set way to do it. For example, if I am a bank and I need to levy a transaction charge of €100,- to a partnership of 3 companies that are equal partners, then what do I do? Charge each €33,34? Charge each €33,33 and eat the cent? Roll some dice, charge one partner at random €33,34, and the other two €33,33? Demand that the parties involved appoint a main account, which gets charged all of it, or if not, the €33,34? Track in each account a residual charge of 0.33333333333333333333333 cents (tossing the remainder away as a rounding error that truly nobody is ever going to care about), even though generally in financial systems, you cannot convey the notion of fractional cents?

There is no right or wrong answer to this question, which means, a / b cannot possibly work here - how could that ever know which of those 5 different things you wanted?

  1. Use the java.math.BigDecimal class, which stores in decimal, and stores perfectly. But note that if you ask BigDecimal to divide 1 by 3, it throws an exception, because that is not possible, so, apply caution.

Upvotes: 1

cactustictacs
cactustictacs

Reputation: 19524

Like people are saying in the comments, Doubles don't actually store a specific number of trailing zeroes - 1.230000000000 and 1.23 are stored the same way. Even if you specify a certain number of zeroes, that info is lost.

If you need to store those numbers with an arbitrary number of trailing zeroes, you should probably store them as Strings - that way they'll display correctly when you print them. Just convert them to Doubles when you want to actually use them as numbers

val numberOne = "123.400"
val numberTwo = "567.80"

// just print them to get the formatting you want
println(numberOne)
// convert to Double to use it
numberOne.toDouble() * 2

Your other option is to create a class that handles this extra data. A simple one would be

data class TrailingDouble(private val asString: String) {
    val value = asString.toDouble()
    override fun toString() = asString
}

where you expose a value property instead of the caller having to call toDouble all the time.

Or you could do something a bit safer

class TrailingDouble private constructor(private val asString: String) {
    // validation done in the instance getters, which are the only thing
    // that can instantiate one of these objects
    val value = asString.toDouble()
    override fun toString() = asString
    
    // override equals etc
    
    companion object {
        fun newInstance(doubleString: String) =
            if (doubleString.toDoubleOrNull() != null) TrailingDouble(doubleString) else null
        // or if you like
        fun String.toTrailingDoubleOrNull() =
            toDoubleOrNull()?.let { TrailingDouble(this) }
    }
}

By using a private constructor, you can guarantee only valid instances of TrailingDouble can be created. That means you can't really use a data class though (you can get around private constructors with the generated copy function) so you'd have to write your own equals and hashCode functions.


And you can do the same approach with an actual Double if you like, specifying the number of trailing zeroes you want:

class TrailingDouble(val value: Double, trailingZeroes: Int) {
    private val asString: String
    
    init {
        val decimalFormat = List(trailingZeroes.coerceAtLeast(1)) { '0' }.joinToString("")
        asString = DecimalFormat("#,###.$decimalFormat").format(value)
    }
    
    override fun toString() = asString
    // equals etc
}

Basically you pass in your double (no need for validation) and the number of trailing zeroes you want, and it produces the appropriate format string to create the string representation.

This one's a lot simpler, but it does require you know how many trailing zeroes you need for a particular Double (again, that information is not contained in the Double itself, it's separate). If you're starting with Strings, the versions that work with a String are probably easier to actually use

Upvotes: 2

deHaar
deHaar

Reputation: 18558

You could simply use String.format(String, Double) here and make the three decimal places fixed via pattern:

fun main() {
    val a: Double = 123.400
    val b: Double = 567.800

    val aStr: String = String.format("%.3f", a)
    val bStr: String = String.format("%.3f", b)

    println("${a.toString()} and ${b.toString()}")
    println("$aStr and $bStr")
}

This will output

123.4 and 567.8
123.400 and 567.800

Upvotes: 3

Related Questions