From 953c475b46c3b5bf70fe2bba22e5680d1542336e Mon Sep 17 00:00:00 2001 From: "J. Fronny" Date: Thu, 7 Dec 2023 19:16:38 +0100 Subject: [PATCH] classlib: implement float support for String.format (#873) --- .../classlib/java/text/TDecimalFormat.java | 16 ++-- .../teavm/classlib/java/util/TFormatter.java | 89 +++++++++++++++++++ .../classlib/java/text/DecimalFormatTest.java | 16 ++++ .../classlib/java/util/FormatterTest.java | 26 ++++++ 4 files changed, 139 insertions(+), 8 deletions(-) diff --git a/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java b/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java index 30f5314e4..9f69d59b4 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java +++ b/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java @@ -245,7 +245,7 @@ public class TDecimalFormat extends TNumberFormat { if (digit >= 0 && digit <= 9) { if (!fractionalPart) { ++intSize; - allowGroupSeparator = groupingSize > 1; + allowGroupSeparator = isGroupingUsed() && groupingSize > 1; } else { ++fracSize; } @@ -383,7 +383,7 @@ public class TDecimalFormat extends TNumberFormat { if (digit >= 0 && digit <= 9) { if (!fractionalPart) { ++intSize; - allowGroupSeparator = groupingSize > 1; + allowGroupSeparator = isGroupingUsed() && groupingSize > 1; } else { ++fracSize; } @@ -710,7 +710,7 @@ public class TDecimalFormat extends TNumberFormat { int digitPos = Math.max(intLength, getMinimumIntegerDigits()) - 1; for (int i = getMinimumIntegerDigits() - 1; i >= intLength; --i) { buffer.append('0'); - if (groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { + if (isGroupingUsed() && groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { buffer.append(symbols.getGroupingSeparator()); } --digitPos; @@ -723,7 +723,7 @@ public class TDecimalFormat extends TNumberFormat { long mantissaDigitMask = POW10_ARRAY[mantissaDigit--]; buffer.append(forDigit(Math.abs((int) (mantissa / mantissaDigitMask)))); mantissa %= mantissaDigitMask; - if (groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { + if (isGroupingUsed() && groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { buffer.append(symbols.getGroupingSeparator()); } --digitPos; @@ -733,7 +733,7 @@ public class TDecimalFormat extends TNumberFormat { intLength -= significantIntDigits; for (int i = 0; i < intLength; ++i) { buffer.append('0'); - if (groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { + if (isGroupingUsed() && groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { buffer.append(symbols.getGroupingSeparator()); } --digitPos; @@ -901,7 +901,7 @@ public class TDecimalFormat extends TNumberFormat { int digitPos = Math.max(intLength, getMinimumIntegerDigits()) - 1; for (int i = getMinimumIntegerDigits() - 1; i >= intLength; --i) { buffer.append('0'); - if (groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { + if (isGroupingUsed() && groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { buffer.append(symbols.getGroupingSeparator()); } --digitPos; @@ -914,7 +914,7 @@ public class TDecimalFormat extends TNumberFormat { BigInteger[] parts = mantissa.divideAndRemainder(mantissaDigitMask); buffer.append(forDigit(Math.abs(parts[0].intValue()))); mantissa = parts[1]; - if (groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { + if (isGroupingUsed() && groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { buffer.append(symbols.getGroupingSeparator()); } --digitPos; @@ -926,7 +926,7 @@ public class TDecimalFormat extends TNumberFormat { intLength -= significantIntDigits; for (int i = 0; i < intLength; ++i) { buffer.append('0'); - if (groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { + if (isGroupingUsed() && groupingSize > 0 && digitPos % groupingSize == 0 && digitPos > 0) { buffer.append(symbols.getGroupingSeparator()); } --digitPos; diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TFormatter.java b/classlib/src/main/java/org/teavm/classlib/java/util/TFormatter.java index 6db23c5ca..fac4340e9 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/TFormatter.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/TFormatter.java @@ -22,6 +22,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; @@ -31,6 +32,7 @@ import java.util.IllegalFormatConversionException; import java.util.Locale; import java.util.UnknownFormatConversionException; import org.teavm.classlib.impl.IntegerUtil; +import org.teavm.classlib.java.text.TDecimalFormat; public final class TFormatter implements Closeable, Flushable { private Locale locale; @@ -240,11 +242,98 @@ public final class TFormatter implements Closeable, Flushable { formatRadixInt(specifier, 4, true); break; + case 'f': + formatFloat(specifier, false); + break; + default: throw new UnknownFormatConversionException(String.valueOf(specifier)); } } + private void formatFloat(char specifier, boolean upperCase) throws IOException { + verifyFlags(specifier, MASK_FOR_INT_DECIMAL_FORMAT); + verifyFloatFlags(); + + if (precision == -1) { + precision = 6; + } + + Object arg = args[argumentIndex]; + boolean negative; + if (arg instanceof Double) { + negative = (Double) arg < 0; + } else if (arg instanceof Float) { + negative = (Float) arg < 0; + } else if (arg instanceof BigDecimal) { + negative = ((BigDecimal) arg).signum() < 0; + } else { + throw new IllegalFormatConversionException(specifier, arg == null ? null : arg.getClass()); + } + + TDecimalFormat format = new TDecimalFormat(); + format.setDecimalFormatSymbols(new DecimalFormatSymbols(locale)); + if (width != -1) { + int decimalSize = predictDecimalSize(negative, format); + format.setMaximumIntegerDigits(decimalSize); + if ((flags & TFormattableFlags.ZERO_PADDED) != 0) { + format.setMinimumIntegerDigits(decimalSize); + } + } + format.setMaximumFractionDigits(precision); + format.setMinimumFractionDigits(precision); + format.setGroupingUsed((flags & TFormattableFlags.GROUPING_SEPARATOR) != 0); + if ((flags & TFormattableFlags.PARENTHESIZED_NEGATIVE) != 0) { + format.setNegativePrefix("("); + format.setNegativeSuffix(")"); + } + if ((flags & TFormattableFlags.SIGNED) != 0) { + format.setPositivePrefix("+"); // DecimalFormatSymbols has no plus sign + } else if ((flags & TFormattableFlags.LEADING_SPACE) != 0) { + format.setPositivePrefix(" "); + } + + String str = format.format(arg); + + precision = -1; // prevent formatGivenString from trimming + + formatGivenString(upperCase, str); + } + + private int predictDecimalSize(boolean negative, TDecimalFormat format) { + int decimalSize = width; + if (precision > 0) { + decimalSize -= precision + 1; // width also includes decimal places. Subtract them! + } + // signs take up space as well. Also subtract them! + if (negative) { + if ((flags & TFormattableFlags.PARENTHESIZED_NEGATIVE) != 0) { + decimalSize -= 2; + } else { + decimalSize--; + } + } else if ((flags & (TFormattableFlags.SIGNED | TFormattableFlags.LEADING_SPACE)) != 0) { + decimalSize--; + } + // the grouping separator also takes up space. You know the drill. + if ((flags & TFormattableFlags.GROUPING_SEPARATOR) != 0) { + decimalSize -= decimalSize / (format.getGroupingSize() + 1); + } + return decimalSize; + } + + private void verifyFloatFlags() { + if ((flags & TFormattableFlags.SIGNED) != 0 && (flags & TFormattableFlags.LEADING_SPACE) != 0) { + throw new TIllegalFormatFlagsException("+ "); + } + if ((flags & TFormattableFlags.ZERO_PADDED) != 0 && (flags & TFormattableFlags.LEFT_JUSTIFY) != 0) { + throw new TIllegalFormatFlagsException("0-"); + } + if ((flags & TFormattableFlags.LEFT_JUSTIFY) != 0 && width < 0) { + throw new TMissingFormatWidthException(format.substring(formatSpecifierStart, index)); + } + } + private void formatBoolean(char specifier, boolean upperCase) throws IOException { verifyFlagsForGeneralFormat(specifier); Object arg = args[argumentIndex]; diff --git a/tests/src/test/java/org/teavm/classlib/java/text/DecimalFormatTest.java b/tests/src/test/java/org/teavm/classlib/java/text/DecimalFormatTest.java index 6a59645de..2080e049e 100644 --- a/tests/src/test/java/org/teavm/classlib/java/text/DecimalFormatTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/text/DecimalFormatTest.java @@ -479,6 +479,22 @@ public class DecimalFormatTest { assertEquals("23.00 RUB", format.format(23)); } + @Test + public void formatsManual() { + DecimalFormat format = new DecimalFormat(); + format.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US)); + format.setMaximumFractionDigits(6); + format.setMinimumFractionDigits(6); + format.setGroupingUsed(false); + assertEquals("1.200000", format.format((Object) 1.2)); + assertEquals("12.200000", format.format((Object) 12.2)); + format.setMaximumFractionDigits(0); + format.setMinimumFractionDigits(0); + format.setMaximumIntegerDigits(5); + format.setMinimumIntegerDigits(5); + assertEquals("00002", format.format((Object) 2.0)); + } + private DecimalFormat createFormat(String format) { return new DecimalFormat(format, symbols); } diff --git a/tests/src/test/java/org/teavm/classlib/java/util/FormatterTest.java b/tests/src/test/java/org/teavm/classlib/java/util/FormatterTest.java index 72218ad9a..a94a542fb 100644 --- a/tests/src/test/java/org/teavm/classlib/java/util/FormatterTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/util/FormatterTest.java @@ -253,4 +253,30 @@ public class FormatterTest { assertEquals(2, e.getPrecision()); } } + + @Test + public void formatsDouble() { + assertEquals("1.200000", new Formatter(Locale.US).format("%f", 1.2).toString()); + assertEquals("12.200000", new Formatter(Locale.US).format("%f", 12.2).toString()); + + assertEquals("00002", new Formatter(Locale.US).format("%05.0f", 2.3).toString()); + assertEquals("-0023", new Formatter(Locale.US).format("%05.0f", -23f).toString()); + assertEquals("1,234.600000", new Formatter(Locale.US).format("%0,9f", 1234.6).toString()); + assertEquals("(1,234.6)", new Formatter(Locale.US).format("%0,(9.1f", -1234.6).toString()); + + assertEquals("1 12 123 1,234 12,345 123,456 1,234,567", new Formatter(Locale.US) + .format("%,.0f %,.0f %,.0f %,.0f %,.0f %,.0f %,.0f", 1f, 12f, 123f, 1234f, 12345f, 123456f, 1234567f) + .toString()); + + assertEquals(" -123.1:-234.2 ", new Formatter(Locale.US) + .format("%7.1f:%-7.1f", -123.1, -234.2).toString()); + + assertEquals("+123.1 +123.2 +0.3", new Formatter(Locale.US) + .format("%+.1f %+05.1f %+.1f", 123.1, 123.2, 0.3).toString()); + + assertEquals(": 123.0:-123.0:", new Formatter(Locale.US) + .format(":% .1f:% .1f:", 123f, -123d).toString()); + + assertEquals("12.050", new Formatter(Locale.US).format("%4.3f", 12.05).toString()); + } }