Reputation: 13027
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
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
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
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