0

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.

Barbiyong
  • 147
  • 1
  • 4
  • 14
  • 1
    Your examples have 1 and 2 trailing zeroes, respectively. What determines the number of trailing zeroes? – Jorn Oct 28 '22 at 13:46
  • 7
    a `double` does not preserve trailing zeros after the comma. `123.400` and `123.4` is exactly the same and not stored differently. – dan1st Oct 28 '22 at 13:46
  • 1
    In other words, the compiler doesn't see any difference between these. It will use the closest binary Double equivalent which has no information about number of decimal places from the original decimal representation that you typed in code. – Tenfour04 Oct 28 '22 at 13:55
  • Obligatory link to [Is floating point math broken?](/questions/588004), which OP should read if they think that either of those doubles is _actually_ the same as the decimal numbers in the code. – gidds Oct 28 '22 at 18:18

3 Answers3

3

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
deHaar
  • 17,687
  • 10
  • 38
  • 51
  • The OP doesn't want three fixed places though, that's what they're trying to avoid. They want to be able to express a Double with an arbitrary number of trailing zeroes, and have that number show up when it's printed – cactustictacs Oct 28 '22 at 19:31
2

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

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
1

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.
rzwitserloot
  • 85,357
  • 5
  • 51
  • 72