Martin Stone
Martin Stone

Reputation: 13027

Optimally format a BigDecimal according to a maximum character limit

I would like to convert a BigDecimal value to a string that it is no longer than a certain number of characters, switching to scientific notation if necessary. How can I do this?

In other words, how can I write the formatDecimal(BigDecimal number, int maxChars) function that would pass the following tests:

    final int maxChars = 6;

    assertEquals(
            "0.3333",
            formatDecimal(new BigDecimal(0.3333333333333), maxChars)
            );

    assertEquals(
            "1.6E+6", 
            formatDecimal(new BigDecimal(1555555), maxChars)
            );

    assertEquals(
            "1.6E-5", 
            formatDecimal(new BigDecimal(0.0000155), maxChars)
            );

    assertEquals(
            "1234.6", 
            formatDecimal(new BigDecimal(1234.56789), maxChars)
            );

    assertEquals(
            "123", 
            formatDecimal(new BigDecimal(123), maxChars)
            );

    assertEquals(
            "0", 
            formatDecimal(new BigDecimal(0), maxChars)
            );

In my application, maxChars doesn't really need to be a parameter -- It will be fixed at compile time (to 18), so a solution using, e.g. DecimalFormat("#.####") could work, so long as the switch to scientific notation can be managed correctly.

Upvotes: 1

Views: 4051

Answers (3)

Martin Stone
Martin Stone

Reputation: 13027

I'm answering my own question here, with a simple brute-force approach that makes me less nervous than those previously posted -- I was hoping that there would be an existing well-tested way of doing this that I was ignorant of.

Unfortunately my solution doesn't handle the third test in the question very well, but I decided I can live with that for my application. In fact I have 18 chars to play with and the 6-character limit in the tests was just intended to make the tests more legible. With 18 chars, I'll get sufficient precision in spite of the leading zeros, until BigDecimal.toString() switches to scientific notation at 10^-6.

Also, this approach is inefficient, but again that's not a problem for my application.

public static String formatDecimal(BigDecimal number, int maxChars) {
    String s;
    int precision = maxChars;
    do {
        s = number
                .round(new MathContext(precision, RoundingMode.HALF_EVEN))
                .stripTrailingZeros()
                .toString();
        --precision;
    } while (s.length() > maxChars && precision > 0);
    return s;
}

Upvotes: 0

jeschafe
jeschafe

Reputation: 2683

public static String formatDecimal(BigDecimal bd, int maxChars)
{
 String convert = bd.toString();
 String result=" ";
 char[] cArray = convert.toCharArray();
 int decPos = -1;
 int chrsBefore = 0;
 int chrsAfter = 0;
 int zeroesAfter = 0;
 int currentPos = 0;
 boolean zeroStreakAlive = true;

 if (cArray.length>maxChars){
    for (char c : cArray){
        if (c=='.')
            decPos = currentPos;
        else if (decPos == -1)
            chrsBefore++;
        else if (zeroStreakAlive && c=='0'){
            zeroesAfter++;
            chrsAfter++;   
        }
        else
            chrsAfter++;
        currentPos++;
    }

    if (chrsBefore>maxChars)
        result = cArray[0] + "." + cArray[1] + "E+" + (chrsBefore-1);
    else if (zeroesAfter >= maxChars-2)
        result = cArray[zeroesAfter+2] + "." + cArray[zeroesAfter+3] + "E-" + (chrsAfter-2);
    else 
        //your logic here probably using DecimalFormat for a normally rounded number to 6 places(including decimal point if one exists).
 }
 return result;
}
}

This is what I have come up with. It's not all encompassing as there are lots of cases to address. I don't know exactly how you want them all to play out so I can't do them all, but you should be able to get the idea and a start from this.

Upvotes: 1

Alex
Alex

Reputation: 25613

A trivial solution if you wanted to limit only the number of decimals would be

public static String formatDecimal(BigDecimal b, int max) {
    return b.setScale(max, RoundingMode.HALF_EVEN).stripTrailingZeros().toEngineeringString();
}

Now, you want also to limit the number of fraction digits depending on the digits in the integer part. I think it is possible to play better with the DecimalFormat and MessageFormat classes, here is what I can come with, it passes the test cases, but I don't think it is that robust. You may try to create a better algorithm to handle all the different cases.

public static String formatDecimal(BigDecimal b, int max) {
    // trivial case
    String bs = b.stripTrailingZeros().toPlainString();
    if (bs.length() <= max) {
        return bs;
    }
    // determine the max integer = 1.0Emax
    String maxInteger = "1" + StringUtils.repeat("0", max - 1);
    // determine the min fraction = 1.0E-max
    String minFraction = "0." + StringUtils.repeat("0", max - 2) + "1";
    // get the integer part
    String integerPart = String.valueOf(b.intValue());
    // make the pattern like ###.### with the correct repetition
    String pattern = StringUtils.repeat("#", max - integerPart.length()) + "." + StringUtils.repeat("#", max - 1 - integerPart.length());
    // play with Message format, using a choice to determine when to use the exponential format
    MessageFormat fmt = new MessageFormat( //
            "{0,choice," + minFraction + "<{0,number,'0.#E0'}|0.1#{0,number,'" + pattern + "'}|" + maxInteger + "<{0,number,'0.#E0'}}" //
    );
    // time to format the number
    return fmt.format(new Object[] {b});
}

Another solution using if/else could be started like this:

public static String formatDecimal(BigDecimal b, int max) {
    // trivial case
    if (b.toPlainString().length() <= max)
        return b.toPlainString();
    else {
        // between 0 and 1, or superior than 1*10^max, better use the exponential notation
        if ((b.compareTo(BigDecimal.ZERO) > 0 && b.compareTo(new BigDecimal("0.01")) < 0) //
                || (b.compareTo(new BigDecimal("1" + StringUtils.repeat("0", max - 1))) > 0)) {
            return new DecimalFormat("#.#E0").format(b);
        } else { // set Scale for fraction, better keep integer part safe
            String sb = b.toPlainString();
            return b.setScale(max - sb.indexOf(".") - 1, RoundingMode.HALF_EVEN).stripTrailingZeros().toPlainString();
        }
    }
}

Upvotes: 1

Related Questions