From ee2d1e7b9b7437527b893580e51fce9652a2ed2b Mon Sep 17 00:00:00 2001 From: Elango Cheran Date: Tue, 26 Nov 2019 15:42:24 -0800 Subject: [PATCH] ICU-13836 Represent suppressed exponent for better plural support --- .../ibm/icu/impl/number/DecimalQuantity.java | 20 ++ .../number/DecimalQuantity_AbstractBCD.java | 86 ++++-- .../DecimalQuantity_DualStorageBCD.java | 5 +- .../com/ibm/icu/number/CompactNotation.java | 8 +- .../ibm/icu/number/ScientificNotation.java | 5 + .../src/com/ibm/icu/text/PluralRules.java | 23 ++ .../impl/number/DecimalQuantity_64BitBCD.java | 1 + .../number/DecimalQuantity_ByteArrayBCD.java | 1 + .../number/DecimalQuantity_SimpleStorage.java | 16 ++ .../icu/dev/test/format/PluralRulesTest.java | 61 ++++ .../dev/test/number/DecimalQuantityTest.java | 271 +++++++++++++++++- .../test/number/NumberFormatterApiTest.java | 9 + 12 files changed, 480 insertions(+), 26 deletions(-) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java index 7d30c58b948..9d6fc349d68 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity.java @@ -122,6 +122,26 @@ public interface DecimalQuantity extends PluralRules.IFixedDecimal { */ public int getMagnitude() throws ArithmeticException; + /** + * @return The value of the (suppressed) exponent after the number has been + * put into a notation with exponents (ex: compact, scientific). Ex: given + * the number 1000 as "1K" / "1E3", the return value will be 3 (positive). + */ + public int getExponent(); + + /** + * Adjusts the value for the (suppressed) exponent stored when using + * notation with exponents (ex: compact, scientific). + * + *

Adjusting the exponent is decoupled from {@link #adjustMagnitude} in + * order to allow flexibility for {@link StandardPlural} to be selected in + * formatting (ex: for compact notation) either with or without the exponent + * applied in the value of the number. + * @param delta + * The value to adjust the exponent by. + */ + public void adjustExponent(int delta); + /** * @return Whether the value represented by this {@link DecimalQuantity} is * zero, infinity, or NaN. diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java index 09c8893513b..32635e5c2d6 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_AbstractBCD.java @@ -86,6 +86,12 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { protected int lReqPos = 0; protected int rReqPos = 0; + /** + * The value of the (suppressed) exponent after the number has been put into + * a notation with exponents (ex: compact, scientific). + */ + protected int exponent = 0; + @Override public void copyFrom(DecimalQuantity _other) { copyBcdFrom(_other); @@ -98,13 +104,14 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { origDouble = other.origDouble; origDelta = other.origDelta; isApproximate = other.isApproximate; + exponent = other.exponent; } public DecimalQuantity_AbstractBCD clear() { lReqPos = 0; rReqPos = 0; flags = 0; - setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, and BCD data + setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, exponent, and BCD data return this; } @@ -216,6 +223,16 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } } + @Override + public int getExponent() { + return exponent; + } + + @Override + public void adjustExponent(int delta) { + exponent = exponent + delta; + } + @Override public StandardPlural getStandardPlural(PluralRules rules) { if (rules == null) { @@ -246,6 +263,8 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { return fractionCount(); case w: return fractionCountWithoutTrailingZeros(); + case e: + return getExponent(); default: return Math.abs(toDouble()); } @@ -291,11 +310,11 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } private int fractionCount() { - return -getLowerDisplayMagnitude(); + return Math.max(0, -getLowerDisplayMagnitude() - exponent); } private int fractionCountWithoutTrailingZeros() { - return Math.max(-scale, 0); + return Math.max(-scale - exponent, 0); } @Override @@ -577,7 +596,9 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { /** * Returns a long approximating the internal BCD. A long can only represent the integral part of the - * number. + * number. Note: this method incorporates the value of {@code exponent} + * (for cases such as compact notation) to return the proper long value + * represented by the result. * * @param truncateIfOverflow if false and the number does NOT fit, fails with an assertion error. * @return A 64-bit integer representation of the internal BCD. @@ -588,12 +609,12 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { // Fallback behavior upon truncateIfOverflow is to truncate at 17 digits. assert(truncateIfOverflow || fitsInLong()); long result = 0L; - int upperMagnitude = scale + precision - 1; + int upperMagnitude = exponent + scale + precision - 1; if (truncateIfOverflow) { upperMagnitude = Math.min(upperMagnitude, 17); } for (int magnitude = upperMagnitude; magnitude >= 0; magnitude--) { - result = result * 10 + getDigitPos(magnitude - scale); + result = result * 10 + getDigitPos(magnitude - scale - exponent); } if (isNegative()) { result = -result; @@ -605,10 +626,13 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { * This returns a long representing the fraction digits of the number, as required by PluralRules. * For example, if we represent the number "1.20" (including optional and required digits), then this * function returns "20" if includeTrailingZeros is true or "2" if false. + * Note: this method incorporates the value of {@code exponent} + * (for cases such as compact notation) to return the proper long value + * represented by the result. */ public long toFractionLong(boolean includeTrailingZeros) { long result = 0L; - int magnitude = -1; + int magnitude = -1 - exponent; int lowerMagnitude = scale; if (includeTrailingZeros) { lowerMagnitude = Math.min(lowerMagnitude, rReqPos); @@ -638,7 +662,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { if (isZeroish()) { return true; } - if (scale < 0) { + if (exponent + scale < 0) { return false; } int magnitude = getMagnitude(); @@ -991,22 +1015,40 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { @Override public String toPlainString() { - // NOTE: This logic is duplicated between here and DecimalQuantity_SimpleStorage. StringBuilder sb = new StringBuilder(); - if (isNegative()) { - sb.append('-'); - } - if (precision == 0 || getMagnitude() < 0) { - sb.append('0'); - } - for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) { - sb.append((char) ('0' + getDigit(m))); - if (m == 0) - sb.append('.'); - } + toPlainString(sb); return sb.toString(); } + public void toPlainString(StringBuilder result) { + assert(!isApproximate); + if (isNegative()) { + result.append('-'); + } + if (precision == 0) { + result.append('0'); + return; + } + + int upper = scale + precision + exponent - 1; + int lower = scale + exponent; + if (upper < lReqPos - 1) { + upper = lReqPos - 1; + } + if (lower > rReqPos) { + lower = rReqPos; + } + + int p = upper; + for (; p >= 0; p--) { + result.append((char) ('0' + getDigitPos(p - scale - exponent))); + } + result.append('.'); + for(; p >= lower; p--) { + result.append((char) ('0' + getDigitPos(p - scale - exponent))); + } + } + public String toScientificString() { StringBuilder sb = new StringBuilder(); toScientificString(sb); @@ -1035,7 +1077,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { } } result.append('E'); - int _scale = upperPos + scale; + int _scale = upperPos + scale + exponent; if (_scale == Integer.MIN_VALUE) { result.append("-2147483648"); return; @@ -1146,7 +1188,7 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity { /** * Sets the internal representation to zero. Clears any values stored in scale, precision, hasDouble, - * origDouble, origDelta, and BCD data. + * origDouble, origDelta, exponent, and BCD data. */ protected abstract void setBcdToZero(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java index 47f0d978c0b..05612af295e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/DecimalQuantity_DualStorageBCD.java @@ -180,6 +180,7 @@ public final class DecimalQuantity_DualStorageBCD extends DecimalQuantity_Abstra isApproximate = false; origDouble = 0; origDelta = 0; + exponent = 0; } @Override @@ -254,11 +255,11 @@ public final class DecimalQuantity_DualStorageBCD extends DecimalQuantity_Abstra } BigDecimal result = BigDecimal.valueOf(tempLong); // Test that the new scale fits inside the BigDecimal - long newScale = result.scale() + scale; + long newScale = result.scale() + scale + exponent; if (newScale <= Integer.MIN_VALUE) { result = BigDecimal.ZERO; } else { - result = result.scaleByPowerOfTen(scale); + result = result.scaleByPowerOfTen(scale + exponent); } if (isNegative()) { result = result.negate(); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java b/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java index a4dad2d49d7..f2bffe7d219 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/CompactNotation.java @@ -128,11 +128,12 @@ public class CompactNotation extends Notation { // Treat zero, NaN, and infinity as if they had magnitude 0 int magnitude; + int multiplier = 0; if (quantity.isZeroish()) { magnitude = 0; micros.rounder.apply(quantity); } else { - int multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data); + multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data); magnitude = quantity.isZeroish() ? 0 : quantity.getMagnitude(); magnitude -= multiplier; } @@ -156,6 +157,11 @@ public class CompactNotation extends Notation { micros.modMiddle = unsafePatternModifier; } + // Change the exponent only after we select appropriate plural form + // for formatting purposes so that we preserve expected formatted + // string behavior. + quantity.adjustExponent(-1 * multiplier); + // We already performed rounding. Do not perform it again. micros.rounder = null; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java b/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java index e751688ca63..cf1ac789888 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/ScientificNotation.java @@ -195,6 +195,11 @@ public class ScientificNotation extends Notation implements Cloneable { micros.modInner = this; } + // Change the exponent only after we select appropriate plural form + // for formatting purposes so that we preserve expected formatted + // string behavior. + quantity.adjustExponent(exponent); + // We already performed rounding. Do not perform it again. micros.rounder = null; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java index a9bfa4657b9..ec6944fa9e7 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java @@ -464,6 +464,16 @@ public class PluralRules implements Serializable { @Deprecated w, + /** + * Suppressed exponent for compact notation (exponent needed in + * scientific notation with compact notation to approximate i). + * + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + e, + /** * THIS OPERAND IS DEPRECATED AND HAS BEEN REMOVED FROM THE SPEC. * @@ -539,6 +549,8 @@ public class PluralRules implements Serializable { private final int baseFactor; + final int suppressedExponent; + /** * @internal CLDR * @deprecated This API is ICU internal only. @@ -620,6 +632,15 @@ public class PluralRules implements Serializable { return baseFactor; } + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public int getSuppressedExponent() { + return suppressedExponent; + } + static final long MAX = (long)1E18; /** @@ -640,6 +661,7 @@ public class PluralRules implements Serializable { ? MAX : (long)n; hasIntegerValue = source == integerValue; + suppressedExponent = 0; // check values. TODO make into unit test. // // long visiblePower = (int) Math.pow(10, v); @@ -797,6 +819,7 @@ public class PluralRules implements Serializable { case t: return decimalDigitsWithoutTrailingZeros; case v: return visibleDecimalDigitCount; case w: return visibleDecimalDigitCountWithoutTrailingZeros; + case e: return suppressedExponent; default: return source; } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_64BitBCD.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_64BitBCD.java index ea84162a983..22f6e20616a 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_64BitBCD.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_64BitBCD.java @@ -95,6 +95,7 @@ public final class DecimalQuantity_64BitBCD extends DecimalQuantity_AbstractBCD isApproximate = false; origDouble = 0; origDelta = 0; + exponent = 0; } @Override diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java index de87080b796..92b40b0023e 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_ByteArrayBCD.java @@ -112,6 +112,7 @@ public final class DecimalQuantity_ByteArrayBCD extends DecimalQuantity_Abstract isApproximate = false; origDouble = 0; origDelta = 0; + exponent = 0; } @Override diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java index cb1bc57755e..24d0434e7a7 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/impl/number/DecimalQuantity_SimpleStorage.java @@ -95,6 +95,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { 1000000000000000000L }; + private int origPrimaryScale; + @Override public int maxRepresentableDigits() { return Integer.MAX_VALUE; @@ -110,6 +112,7 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { primaryScale = 0; primaryPrecision = computePrecision(primary); fallback = null; + origPrimaryScale = primaryScale; } /** @@ -189,6 +192,8 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { primary = -1; fallback = new BigDecimal(temp); } + + origPrimaryScale = primaryScale; } static final double LOG_2_OF_TEN = 3.32192809489; @@ -279,6 +284,7 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { primaryPrecision = _other.primaryPrecision; fallback = _other.fallback; flags = _other.flags; + origPrimaryScale = _other.origPrimaryScale; } @Override @@ -916,4 +922,14 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity { .setFractionDigits((int) getPluralOperand(Operand.v), (long) getPluralOperand(Operand.f)); } } + + @Override + public int getExponent() { + return origPrimaryScale; + } + + @Override + public void adjustExponent(int delta) { + origPrimaryScale = origPrimaryScale + delta; + } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java index 2379470e7d9..b67000e0c8b 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/PluralRulesTest.java @@ -42,6 +42,7 @@ import com.ibm.icu.dev.util.CollectionUtilities; import com.ibm.icu.impl.Relation; import com.ibm.icu.impl.Utility; import com.ibm.icu.number.FormattedNumber; +import com.ibm.icu.number.LocalizedNumberFormatter; import com.ibm.icu.number.NumberFormatter; import com.ibm.icu.number.Precision; import com.ibm.icu.number.UnlocalizedNumberFormatter; @@ -929,6 +930,66 @@ public class PluralRulesTest extends TestFmwk { } } + + + @Test + public void testCompactDecimalPluralKeyword() { + PluralRules rules = PluralRules.createRules("one: i = 0,1 @integer 0, 1 @decimal 0.0~1.5; many: e = 0 and i % 1000000 = 0 and v = 0 or " + + "e != 0 .. 5; other: @integer 2~17, 100, 1000, 10000, 100000, 1000000, @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"); + ULocale locale = new ULocale("fr-FR"); + + Object[][] casesData = { + // unlocalized formatter skeleton, input, string output, plural rule keyword + {"", 0, "0", "one"}, + {"compact-long", 0, "0", "one"}, + + {"", 1, "1", "one"}, + {"compact-long", 1, "1", "one"}, + + {"", 2, "2", "other"}, + {"compact-long", 2, "2", "other"}, + + {"", 1000000, "1 000 000", "many"}, + {"compact-long", 1000000, "1 million", "many"}, + + {"", 1000001, "1 000 001", "other"}, + {"compact-long", 1000001, "1 million", "many"}, + + {"", 120000, "1 200 000", "other"}, + {"compact-long", 1200000, "1,2 millions", "many"}, + + {"", 1200001, "1 200 001", "other"}, + {"compact-long", 1200001, "1,2 millions", "many"}, + + {"", 2000000, "2 000 000", "many"}, + {"compact-long", 2000000, "2 millions", "many"}, + }; + + for (Object[] caseDatum : casesData) { + String skeleton = (String) caseDatum[0]; + int input = (int) caseDatum[1]; + String expectedString = (String) caseDatum[2]; + String expectPluralRuleKeyword = (String) caseDatum[3]; + + String actualPluralRuleKeyword = + getPluralKeyword(rules, locale, input, skeleton); + + assertEquals( + String.format("PluralRules select %s: %d", skeleton, input), + expectPluralRuleKeyword, + actualPluralRuleKeyword); + } + } + + private String getPluralKeyword(PluralRules rules, ULocale locale, double number, String skeleton) { + LocalizedNumberFormatter formatter = + NumberFormatter.forSkeleton(skeleton) + .locale(locale); + FormattedNumber fn = formatter.format(number); + String pluralKeyword = rules.select(fn); + return pluralKeyword; + } + enum StandardPluralCategories { zero, one, two, few, many, other; /** diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java index 471682a22ab..82a0b4f4396 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/DecimalQuantityTest.java @@ -24,10 +24,15 @@ import com.ibm.icu.impl.number.DecimalFormatProperties; import com.ibm.icu.impl.number.DecimalQuantity; import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; import com.ibm.icu.impl.number.RoundingUtils; +import com.ibm.icu.number.FormattedNumber; import com.ibm.icu.number.LocalizedNumberFormatter; +import com.ibm.icu.number.Notation; import com.ibm.icu.number.NumberFormatter; +import com.ibm.icu.number.Precision; +import com.ibm.icu.number.Scale; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; import com.ibm.icu.text.DecimalFormatSymbols; +import com.ibm.icu.text.PluralRules.Operand; import com.ibm.icu.util.ULocale; @RunWith(JUnit4.class) @@ -603,8 +608,272 @@ public class DecimalQuantityTest extends TestFmwk { } } + @Test + public void testCompactDecimalSuppressedExponent() { + ULocale locale = new ULocale("fr-FR"); + + Object[][] casesData = { + // unlocalized formatter skeleton, input, string output, long output, double output, BigDecimal output, plain string, suppressed exponent + {"", 123456789, "123 456 789", 123456789L, 123456789.0, new BigDecimal("123456789"), "123456789.", 0}, + {"compact-long", 123456789, "123 millions", 123000000L, 123000000.0, new BigDecimal("123000000"), "123000000.", 6}, + {"compact-short", 123456789, "123 M", 123000000L, 123000000.0, new BigDecimal("123000000"), "123000000.", 6}, + {"scientific", 123456789, "1,234568E8", 123456800L, 123456800.0, new BigDecimal("123456800"), "123456800.", 8}, + + {"", 1234567, "1 234 567", 1234567L, 1234567.0, new BigDecimal("1234567"), "1234567.", 0}, + {"compact-long", 1234567, "1,2 million", 1200000L, 1200000.0, new BigDecimal("1200000"), "1200000.", 6}, + {"compact-short", 1234567, "1,2 M", 1200000L, 1200000.0, new BigDecimal("1200000"), "1200000.", 6}, + {"scientific", 1234567, "1,234567E6", 1234567L, 1234567.0, new BigDecimal("1234567"), "1234567.", 6}, + + {"", 123456, "123 456", 123456L, 123456.0, new BigDecimal("123456"), "123456.", 0}, + {"compact-long", 123456, "123 mille", 123000L, 123000.0, new BigDecimal("123000"), "123000.", 3}, + {"compact-short", 123456, "123 k", 123000L, 123000.0, new BigDecimal("123000"), "123000.", 3}, + {"scientific", 123456, "1,23456E5", 123456L, 123456.0, new BigDecimal("123456"), "123456.", 5}, + + {"", 123, "123", 123L, 123.0, new BigDecimal("123"), "123.", 0}, + {"compact-long", 123, "123", 123L, 123.0, new BigDecimal("123"), "123.", 0}, + {"compact-short", 123, "123", 123L, 123.0, new BigDecimal("123"), "123.", 0}, + {"scientific", 123, "1,23E2", 123L, 123.0, new BigDecimal("123"), "123.", 2}, + + {"", 1.2, "1,2", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + {"compact-long", 1.2, "1,2", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + {"compact-short", 1.2, "1,2", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + {"scientific", 1.2, "1,2E0", 1L, 1.2, new BigDecimal("1.2"), "1.2", 0}, + + {"", 0.12, "0,12", 0L, 0.12, new BigDecimal("0.12"), "0.12", 0}, + {"compact-long", 0.12, "0,12", 0L, 0.12, new BigDecimal("0.12"), "0.12", 0}, + {"compact-short", 0.12, "0,12", 0L, 0.12, new BigDecimal("0.12"), "0.12", 0}, + {"scientific", 0.12, "1,2E-1", 0L, 0.12, new BigDecimal("0.12"), "0.12", -1}, + + {"", 0.012, "0,012", 0L, 0.012, new BigDecimal("0.012"), "0.012", 0}, + {"compact-long", 0.012, "0,012", 0L, 0.012, new BigDecimal("0.012"), "0.012", 0}, + {"compact-short", 0.012, "0,012", 0L, 0.012, new BigDecimal("0.012"), "0.012", 0}, + {"scientific", 0.012, "1,2E-2", 0L, 0.012, new BigDecimal("0.012"), "0.012", -2}, + + {"", 999.9, "999,9", 999L, 999.9, new BigDecimal("999.9"), "999.9", 0}, + {"compact-long", 999.9, "1 millier", 1000L, 1000.0, new BigDecimal("1000"), "1000.", 3}, + {"compact-short", 999.9, "1 k", 1000L, 1000.0, new BigDecimal("1000"), "1000.", 3}, + {"scientific", 999.9, "9,999E2", 999L, 999.9, new BigDecimal("999.9"), "999.9", 2}, + + {"", 1000.0, "1 000", 1000L, 1000.0, new BigDecimal("1000"), "1000.", 0}, + {"compact-long", 1000.0, "1 millier", 1000L, 1000.0, new BigDecimal("1000"), "1000.", 3}, + {"compact-short", 1000.0, "1 k", 1000L, 1000.0, new BigDecimal("1000"), "1000.", 3}, + {"scientific", 1000.0, "1E3", 1000L, 1000.0, new BigDecimal("1000"), "1000.", 3}, + }; + + for (Object[] caseDatum : casesData) { + // test the helper methods used to compute plural operand values + + String skeleton = (String) caseDatum[0]; + LocalizedNumberFormatter formatter = + NumberFormatter.forSkeleton(skeleton) + .locale(locale); + double input = ((Number) caseDatum[1]).doubleValue(); + String expectedString = (String) caseDatum[2]; + long expectedLong = (long) caseDatum[3]; + double expectedDouble = (double) caseDatum[4]; + BigDecimal expectedBigDecimal = (BigDecimal) caseDatum[5]; + String expectedPlainString = (String) caseDatum[6]; + int expectedSuppressedExponent = (int) caseDatum[7]; + + FormattedNumber fn = formatter.format(input); + DecimalQuantity_DualStorageBCD dq = (DecimalQuantity_DualStorageBCD) + fn.getFixedDecimal(); + String actualString = fn.toString(); + long actualLong = dq.toLong(true); + double actualDouble = dq.toDouble(); + BigDecimal actualBigDecimal = dq.toBigDecimal(); + String actualPlainString = dq.toPlainString(); + int actualSuppressedExponent = dq.getExponent(); + + assertEquals( + String.format("formatted number %s toString: %f", skeleton, input), + expectedString, + actualString); + assertEquals( + String.format("compact decimal %s toLong: %f", skeleton, input), + expectedLong, + actualLong); + assertDoubleEquals( + String.format("compact decimal %s toDouble: %f", skeleton, input), + expectedDouble, + actualDouble); + assertBigDecimalEquals( + String.format("compact decimal %s toBigDecimal: %f", skeleton, input), + expectedBigDecimal, + actualBigDecimal); + assertEquals( + String.format("formatted number %s toPlainString: %f", skeleton, input), + expectedPlainString, + actualPlainString); + assertEquals( + String.format("compact decimal %s suppressed exponent: %f", skeleton, input), + expectedSuppressedExponent, + actualSuppressedExponent); + + // test the actual computed values of the plural operands + + double expectedNOperand = expectedDouble; + double expectedIOperand = expectedLong; + double expectedEOperand = expectedSuppressedExponent; + double actualNOperand = dq.getPluralOperand(Operand.n); + double actualIOperand = dq.getPluralOperand(Operand.i); + double actualEOperand = dq.getPluralOperand(Operand.e); + + assertEquals( + String.format("formatted number %s toString: %s", skeleton, input), + expectedString, + actualString); + assertDoubleEquals( + String.format("compact decimal %s n operand: %f", skeleton, input), + expectedNOperand, + actualNOperand); + assertDoubleEquals( + String.format("compact decimal %s i operand: %f", skeleton, input), + expectedIOperand, + actualIOperand); + assertDoubleEquals( + String.format("compact decimal %s e operand: %f", skeleton, input), + expectedEOperand, + actualEOperand); + } + } + + + @Test + public void testCompactNotationFractionPluralOperands() { + ULocale locale = new ULocale("fr-FR"); + LocalizedNumberFormatter formatter = + NumberFormatter.withLocale(locale) + .notation(Notation.compactLong()) + .precision(Precision.fixedFraction(5)) + .scale(Scale.powerOfTen(-1)); + double formatterInput = 12345; + double inputVal = 1234.5; + FormattedNumber fn = formatter.format(formatterInput); + DecimalQuantity_DualStorageBCD dq = (DecimalQuantity_DualStorageBCD) + fn.getFixedDecimal(); + + double expectedNOperand = 1234.5; + double expectedIOperand = 1234; + double expectedFOperand = 50; + double expectedTOperand = 5; + double expectedVOperand = 2; + double expectedWOperand = 1; + double expectedEOperand = 3; + String expectedString = "1,23450 millier"; + double actualNOperand = dq.getPluralOperand(Operand.n); + double actualIOperand = dq.getPluralOperand(Operand.i); + double actualFOperand = dq.getPluralOperand(Operand.f); + double actualTOperand = dq.getPluralOperand(Operand.t); + double actualVOperand = dq.getPluralOperand(Operand.v); + double actualWOperand = dq.getPluralOperand(Operand.w); + double actualEOperand = dq.getPluralOperand(Operand.e); + String actualString = fn.toString(); + + assertDoubleEquals( + String.format("compact decimal fraction n operand: %f", inputVal), + expectedNOperand, + actualNOperand); + assertDoubleEquals( + String.format("compact decimal fraction i operand: %f", inputVal), + expectedIOperand, + actualIOperand); + assertDoubleEquals( + String.format("compact decimal fraction f operand: %f", inputVal), + expectedFOperand, + actualFOperand); + assertDoubleEquals( + String.format("compact decimal fraction t operand: %f", inputVal), + expectedTOperand, + actualTOperand); + assertDoubleEquals( + String.format("compact decimal fraction v operand: %f", inputVal), + expectedVOperand, + actualVOperand); + assertDoubleEquals( + String.format("compact decimal fraction w operand: %f", inputVal), + expectedWOperand, + actualWOperand); + assertDoubleEquals( + String.format("compact decimal fraction e operand: %f", inputVal), + expectedEOperand, + actualEOperand); + assertEquals( + String.format("compact decimal fraction toString: %f", inputVal), + expectedString, + actualString); + } + + @Test + public void testSuppressedExponentUnchangedByInitialScaling() { + ULocale locale = new ULocale("fr-FR"); + LocalizedNumberFormatter withLocale = NumberFormatter.withLocale(locale); + LocalizedNumberFormatter compactLong = + withLocale.notation(Notation.compactLong()); + LocalizedNumberFormatter compactScaled = + compactLong.scale(Scale.powerOfTen(3)); + + Object[][] casesData = { + // input, compact long string output, + // compact n operand, compact i operand, compact e operand + {123456789, "123 millions", 123000000.0, 123000000.0, 6.0}, + {1234567, "1,2 million", 1200000.0, 1200000.0, 6.0}, + {123456, "123 mille", 123000.0, 123000.0, 3.0}, + {123, "123", 123.0, 123.0, 0.0}, + }; + + for (Object[] caseDatum : casesData) { + int input = (int) caseDatum[0]; + String expectedString = (String) caseDatum[1]; + double expectedNOperand = (double) caseDatum[2]; + double expectedIOperand = (double) caseDatum[3]; + double expectedEOperand = (double) caseDatum[4]; + + FormattedNumber fnCompactScaled = compactScaled.format(input); + DecimalQuantity_DualStorageBCD dqCompactScaled = + (DecimalQuantity_DualStorageBCD) fnCompactScaled.getFixedDecimal(); + double compactScaledEOperand = dqCompactScaled.getPluralOperand(Operand.e); + + FormattedNumber fnCompact = compactLong.format(input); + DecimalQuantity_DualStorageBCD dqCompact = + (DecimalQuantity_DualStorageBCD) fnCompact.getFixedDecimal(); + String actualString = fnCompact.toString(); + double compactNOperand = dqCompact.getPluralOperand(Operand.n); + double compactIOperand = dqCompact.getPluralOperand(Operand.i); + double compactEOperand = dqCompact.getPluralOperand(Operand.e); + assertEquals( + String.format("formatted number compactLong toString: %s", input), + expectedString, + actualString); + assertDoubleEquals( + String.format("compact decimal %d, n operand vs. expected", input), + expectedNOperand, + compactNOperand); + assertDoubleEquals( + String.format("compact decimal %d, i operand vs. expected", input), + expectedIOperand, + compactIOperand); + assertDoubleEquals( + String.format("compact decimal %d, e operand vs. expected", input), + expectedEOperand, + compactEOperand); + + // By scaling by 10^3 in a locale that has words / compact notation + // based on powers of 10^3, we guarantee that the suppressed + // exponent will differ by 3. + assertDoubleEquals( + String.format("decimal %d, e operand for compact vs. compact scaled", input), + compactEOperand + 3, + compactScaledEOperand); + } + } + + static boolean doubleEquals(double d1, double d2) { + return (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6); + } + static void assertDoubleEquals(String message, double d1, double d2) { - boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6); + boolean equal = doubleEquals(d1, d2); handleAssert(equal, message, d1, d2, null, false); } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java index 4ba49dcdafb..47000e6f390 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterApiTest.java @@ -349,6 +349,15 @@ public class NumberFormatterApiTest { 1000000, "1 millón"); + assertFormatSingle( + "Compact Plural One with rounding", + "compact-long precision-integer", + "KK precision-integer", + NumberFormatter.with().notation(Notation.compactLong()).precision(Precision.integer()), + ULocale.forLanguageTag("es"), + 1222222, + "1 millón"); + assertFormatSingle( "Compact Plural Other", "compact-long",