ICU-20019 Implement withSignificantDigits option
See #1598
diff --git a/docs/userguide/format_parse/numbers/skeletons.md b/docs/userguide/format_parse/numbers/skeletons.md
index 227ea30..fd5e2e9 100644
--- a/docs/userguide/format_parse/numbers/skeletons.md
+++ b/docs/userguide/format_parse/numbers/skeletons.md
@@ -219,20 +219,26 @@
Note that the stem `.` is considered valid and is equivalent to `precision-integer`.
-Fraction-precision stems accept a single optional option: the minimum or
-maximum number of significant digits. This allows you to combine fraction
-precision with certain significant digits capabilities. The following are
-examples:
+Fraction-precision stems accept a single optional option: a number of significant digits.
+The options here correspond to the API functions on `FractionPrecision`. Some options
+require specifying `r` or `s` for relaxed mode or strict mode. For more information, see
+the API docs for UNumberRoundingPriority.
| Skeleton | Explanation | Equivalent C++ Code |
|---|---|---|
| `.##/@@@*` | At most 2 fraction digits, but guarantee <br/> at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withMinDigits(3)` |
+| `.##/@##r` | Same as above | `Precision::maxFraction(2)` <br/> `.withSignificantDigits(1, 3, RELAXED)` |
+| `.##/@@@r` | Same as above, but pad trailing zeros <br/> to at least 3 significant digits | `Precision::maxFraction(2)` <br/> `.withSignificantDigits(3, 3, RELAXED)` |
| `.00/@##` | Exactly 2 fraction digits, but do not <br/> display more than 3 significant digits | `Precision::fixedFraction(2)` <br/> `.withMaxDigits(3)` |
+| `.00/@##s` | Same as above | `Precision::fixedFraction(2)` <br/> `.withSignificantDigits(1, 3, STRICT)` |
+| `.00/@##s` | Same as above, but pad trailing zeros <br/> to at least 3 significant digits | `Precision::fixedFraction(2)` <br/> `.withSignificantDigits(3, 3, STRICT)` |
-Precisely, the option starts with one or more `@` symbols. Then it contains
-either a `*`, for `::withMinDigits`, or one or more `#` symbols, for
-`::withMaxDigits`. If a `#` symbol is present, there must be only one `@`
-symbol.
+Precisely, the option follows the syntax of the significant digits stem (see below),
+but one of the following must be true:
+
+- Option has one or more `@`s followed by the wildcard character (`withMinDigits`)
+- Option has exactly one `@` followed by zero or more `#`s (`withMaxDigits`)
+- Option has one or more `@`s followed by zero or more `#`s and ends in `s` or `r` (`withSignificantDigits`)
#### Significant Digits Precision
diff --git a/icu4c/source/i18n/number_rounding.cpp b/icu4c/source/i18n/number_rounding.cpp
index a8fd6bc..0932d2f 100644
--- a/icu4c/source/i18n/number_rounding.cpp
+++ b/icu4c/source/i18n/number_rounding.cpp
@@ -205,10 +205,32 @@
return constructCurrency(currencyUsage);
}
+Precision FractionPrecision::withSignificantDigits(
+ int32_t minSignificantDigits,
+ int32_t maxSignificantDigits,
+ UNumberRoundingPriority priority) const {
+ if (fType == RND_ERROR) { return *this; } // no-op in error state
+ if (minSignificantDigits >= 1 &&
+ maxSignificantDigits >= minSignificantDigits &&
+ maxSignificantDigits <= kMaxIntFracSig) {
+ return constructFractionSignificant(
+ *this,
+ minSignificantDigits,
+ maxSignificantDigits,
+ priority);
+ } else {
+ return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
+ }
+}
+
Precision FractionPrecision::withMinDigits(int32_t minSignificantDigits) const {
if (fType == RND_ERROR) { return *this; } // no-op in error state
if (minSignificantDigits >= 1 && minSignificantDigits <= kMaxIntFracSig) {
- return constructFractionSignificant(*this, minSignificantDigits, -1);
+ return constructFractionSignificant(
+ *this,
+ 1,
+ minSignificantDigits,
+ UNUM_ROUNDING_PRIORITY_RELAXED);
} else {
return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
}
@@ -217,7 +239,10 @@
Precision FractionPrecision::withMaxDigits(int32_t maxSignificantDigits) const {
if (fType == RND_ERROR) { return *this; } // no-op in error state
if (maxSignificantDigits >= 1 && maxSignificantDigits <= kMaxIntFracSig) {
- return constructFractionSignificant(*this, -1, maxSignificantDigits);
+ return constructFractionSignificant(*this,
+ 1,
+ maxSignificantDigits,
+ UNUM_ROUNDING_PRIORITY_STRICT);
} else {
return {U_NUMBER_ARG_OUTOFBOUNDS_ERROR};
}
@@ -280,10 +305,15 @@
}
Precision
-Precision::constructFractionSignificant(const FractionPrecision &base, int32_t minSig, int32_t maxSig) {
+Precision::constructFractionSignificant(
+ const FractionPrecision &base,
+ int32_t minSig,
+ int32_t maxSig,
+ UNumberRoundingPriority priority) {
FractionSignificantSettings settings = base.fUnion.fracSig;
settings.fMinSig = static_cast<digits_t>(minSig);
settings.fMaxSig = static_cast<digits_t>(maxSig);
+ settings.fPriority = priority;
PrecisionUnion union_;
union_.fracSig = settings;
return {RND_FRACTION_SIGNIFICANT, union_};
@@ -417,23 +447,21 @@
break;
case Precision::RND_FRACTION_SIGNIFICANT: {
- int32_t displayMag = getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac);
- int32_t roundingMag = getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac);
- if (fPrecision.fUnion.fracSig.fMinSig == -1) {
- // Max Sig override
- int32_t candidate = getRoundingMagnitudeSignificant(
- value,
- fPrecision.fUnion.fracSig.fMaxSig);
- roundingMag = uprv_max(roundingMag, candidate);
+ int32_t roundingMag1 = getRoundingMagnitudeFraction(fPrecision.fUnion.fracSig.fMaxFrac);
+ int32_t roundingMag2 = getRoundingMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMaxSig);
+ int32_t roundingMag;
+ if (fPrecision.fUnion.fracSig.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+ roundingMag = uprv_min(roundingMag1, roundingMag2);
} else {
- // Min Sig override
- int32_t candidate = getDisplayMagnitudeSignificant(
- value,
- fPrecision.fUnion.fracSig.fMinSig);
- roundingMag = uprv_min(roundingMag, candidate);
+ roundingMag = uprv_max(roundingMag1, roundingMag2);
}
value.roundToMagnitude(roundingMag, fRoundingMode, status);
+
+ int32_t displayMag1 = getDisplayMagnitudeFraction(fPrecision.fUnion.fracSig.fMinFrac);
+ int32_t displayMag2 = getDisplayMagnitudeSignificant(value, fPrecision.fUnion.fracSig.fMinSig);
+ int32_t displayMag = uprv_min(displayMag1, displayMag2);
value.setMinFraction(uprv_max(0, -displayMag));
+
break;
}
diff --git a/icu4c/source/i18n/number_skeletons.cpp b/icu4c/source/i18n/number_skeletons.cpp
index 5aae555..a07725b 100644
--- a/icu4c/source/i18n/number_skeletons.cpp
+++ b/icu4c/source/i18n/number_skeletons.cpp
@@ -1280,21 +1280,14 @@
break;
}
}
- // For the frac-sig option, there must be minSig or maxSig but not both.
- // Valid: @+, @@+, @@@+
- // Valid: @#, @##, @###
- // Invalid: @, @@, @@@
- // Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
if (isWildcardChar(segment.charAt(offset))) {
+ // @+, @@+, @@@+
maxSig = -1;
offset++;
- } else if (minSig > 1) {
- // @@#, @@##, @@@#
- // throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
- status = U_NUMBER_SKELETON_SYNTAX_ERROR;
- return false;
} else {
+ // @#, @##, @###
+ // @@#, @@##, @@@#
maxSig = minSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == u'#') {
@@ -1306,22 +1299,45 @@
}
} else {
// @, @@, @@@
- // throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
- status = U_NUMBER_SKELETON_SYNTAX_ERROR;
- return false;
+ maxSig = minSig;
}
+ UNumberRoundingPriority priority;
if (offset < segment.length()) {
- // throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+ if (maxSig == -1) {
+ // The wildcard character is not allowed with the priority annotation
+ status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+ return false;
+ }
+ if (segment.codePointAt(offset) == u'r') {
+ priority = UNUM_ROUNDING_PRIORITY_RELAXED;
+ offset++;
+ } else if (segment.codePointAt(offset) == u's') {
+ priority = UNUM_ROUNDING_PRIORITY_STRICT;
+ offset++;
+ } else {
+ U_ASSERT(offset < segment.length());
+ }
+ if (offset < segment.length()) {
+ // Invalid digits option for fraction rounder
+ status = U_NUMBER_SKELETON_SYNTAX_ERROR;
+ return false;
+ }
+ } else if (maxSig == -1) {
+ // withMinDigits
+ maxSig = minSig;
+ minSig = 1;
+ priority = UNUM_ROUNDING_PRIORITY_RELAXED;
+ } else if (minSig == 1) {
+ // withMaxDigits
+ priority = UNUM_ROUNDING_PRIORITY_STRICT;
+ } else {
+ // Digits options with both min and max sig require the priority option
status = U_NUMBER_SKELETON_SYNTAX_ERROR;
return false;
}
auto& oldPrecision = static_cast<const FractionPrecision&>(macros.precision);
- if (maxSig == -1) {
- macros.precision = oldPrecision.withMinDigits(minSig);
- } else {
- macros.precision = oldPrecision.withMaxDigits(maxSig);
- }
+ macros.precision = oldPrecision.withSignificantDigits(minSig, maxSig, priority);
return true;
}
@@ -1552,10 +1568,11 @@
const Precision::FractionSignificantSettings& impl = macros.precision.fUnion.fracSig;
blueprint_helpers::generateFractionStem(impl.fMinFrac, impl.fMaxFrac, sb, status);
sb.append(u'/');
- if (impl.fMinSig == -1) {
- blueprint_helpers::generateDigitsStem(1, impl.fMaxSig, sb, status);
+ blueprint_helpers::generateDigitsStem(impl.fMinSig, impl.fMaxSig, sb, status);
+ if (impl.fPriority == UNUM_ROUNDING_PRIORITY_RELAXED) {
+ sb.append(u'r');
} else {
- blueprint_helpers::generateDigitsStem(impl.fMinSig, -1, sb, status);
+ sb.append(u's');
}
} else if (macros.precision.fType == Precision::RND_INCREMENT
|| macros.precision.fType == Precision::RND_INCREMENT_ONE
diff --git a/icu4c/source/i18n/unicode/numberformatter.h b/icu4c/source/i18n/unicode/numberformatter.h
index 25239c3..10e4830 100644
--- a/icu4c/source/i18n/unicode/numberformatter.h
+++ b/icu4c/source/i18n/unicode/numberformatter.h
@@ -694,6 +694,8 @@
impl::digits_t fMinSig;
/** @internal */
impl::digits_t fMaxSig;
+ /** @internal */
+ UNumberRoundingPriority fPriority;
} fracSig;
/** @internal */
struct IncrementSettings {
@@ -740,8 +742,11 @@
static Precision constructSignificant(int32_t minSig, int32_t maxSig);
- static Precision
- constructFractionSignificant(const FractionPrecision &base, int32_t minSig, int32_t maxSig);
+ static Precision constructFractionSignificant(
+ const FractionPrecision &base,
+ int32_t minSig,
+ int32_t maxSig,
+ UNumberRoundingPriority priority);
static IncrementPrecision constructIncrement(double increment, int32_t minFrac);
@@ -783,16 +788,38 @@
*/
class U_I18N_API FractionPrecision : public Precision {
public:
+#ifndef U_HIDE_DRAFT_API
/**
- * Ensure that no less than this number of significant digits are retained when rounding according to fraction
- * rules.
+ * Override maximum fraction digits with maximum significant digits depending on the magnitude
+ * of the number. See UNumberRoundingPriority.
*
- * <p>
- * For example, with integer rounding, the number 3.141 becomes "3". However, with minimum figures set to 2, 3.141
- * becomes "3.1" instead.
+ * @param minSignificantDigits
+ * Pad trailing zeros to achieve this minimum number of significant digits.
+ * @param maxSignificantDigits
+ * Round the number to achieve this maximum number of significant digits.
+ * @param priority
+ * How to disambiguate between fraction digits and significant digits.
+ * @return A precision for chaining or passing to the NumberFormatter precision() setter.
*
- * <p>
- * This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3", not "3.0".
+ * @draft ICU 69
+ */
+ Precision withSignificantDigits(
+ int32_t minSignificantDigits,
+ int32_t maxSignificantDigits,
+ UNumberRoundingPriority priority) const;
+#endif // U_HIDE_DRAFT_API
+
+ /**
+ * Ensure that no less than this number of significant digits are retained when rounding
+ * according to fraction rules.
+ *
+ * For example, with integer rounding, the number 3.141 becomes "3". However, with minimum
+ * figures set to 2, 3.141 becomes "3.1" instead.
+ *
+ * This setting does not affect the number of trailing zeros. For example, 3.01 would print as
+ * "3", not "3.0".
+ *
+ * This is equivalent to `withSignificantDigits(1, minSignificantDigits, RELAXED)`.
*
* @param minSignificantDigits
* The number of significant figures to guarantee.
@@ -802,16 +829,16 @@
Precision withMinDigits(int32_t minSignificantDigits) const;
/**
- * Ensure that no more than this number of significant digits are retained when rounding according to fraction
- * rules.
+ * Ensure that no more than this number of significant digits are retained when rounding
+ * according to fraction rules.
*
- * <p>
- * For example, with integer rounding, the number 123.4 becomes "123". However, with maximum figures set to 2, 123.4
- * becomes "120" instead.
+ * For example, with integer rounding, the number 123.4 becomes "123". However, with maximum
+ * figures set to 2, 123.4 becomes "120" instead.
*
- * <p>
- * This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2, 123.4 would
- * become "120.00".
+ * This setting does not affect the number of trailing zeros. For example, with fixed fraction
+ * of 2, 123.4 would become "120.00".
+ *
+ * This is equivalent to `withSignificantDigits(1, maxSignificantDigits, STRICT)`.
*
* @param maxSignificantDigits
* Round the number to no more than this number of significant figures.
diff --git a/icu4c/source/i18n/unicode/unumberformatter.h b/icu4c/source/i18n/unicode/unumberformatter.h
index 71bbc58..1430f65 100644
--- a/icu4c/source/i18n/unicode/unumberformatter.h
+++ b/icu4c/source/i18n/unicode/unumberformatter.h
@@ -78,6 +78,62 @@
* </pre>
*/
+#ifndef U_FORCE_HIDE_DRAFT_API
+/**
+ * An enum declaring how to resolve conflicts between maximum fraction digits and maximum
+ * significant digits.
+ *
+ * There are two modes, RELAXED and STRICT:
+ *
+ * - RELAXED: Relax one of the two constraints (fraction digits or significant digits) in order
+ * to round the number to a higher level of precision.
+ * - STRICT: Enforce both constraints, resulting in the number being rounded to a lower
+ * level of precision.
+ *
+ * The default settings for compact notation rounding are Max-Fraction = 0 (round to the nearest
+ * integer), Max-Significant = 2 (round to 2 significant digits), and priority RELAXED (choose
+ * the constraint that results in more digits being displayed).
+ *
+ * Conflicting *minimum* fraction and significant digits are always resolved in the direction that
+ * results in more trailing zeros.
+ *
+ * Example 1: Consider the number 3.141, with various different settings:
+ *
+ * - Max-Fraction = 1: "3.1"
+ * - Max-Significant = 3: "3.14"
+ *
+ * The rounding priority determines how to resolve the conflict when both Max-Fraction and
+ * Max-Significant are set. With RELAXED, the less-strict setting (the one that causes more digits
+ * to be displayed) will be used; Max-Significant wins. With STRICT, the more-strict setting (the
+ * one that causes fewer digits to be displayed) will be used; Max-Fraction wins.
+ *
+ * Example 2: Consider the number 8317, with various different settings:
+ *
+ * - Max-Fraction = 1: "8317"
+ * - Max-Significant = 3: "8320"
+ *
+ * Here, RELAXED favors Max-Fraction and STRICT favors Max-Significant. Note that this larger
+ * number caused the two modes to favor the opposite result.
+ *
+ * @draft ICU 69
+ */
+typedef enum UNumberRoundingPriority {
+ /**
+ * Favor greater precision by relaxing one of the rounding constraints.
+ *
+ * @draft ICU 69
+ */
+ UNUM_ROUNDING_PRIORITY_RELAXED,
+
+ /**
+ * Favor adherence to all rounding constraints by producing lower precision.
+ *
+ * @draft ICU 69
+ */
+ UNUM_ROUNDING_PRIORITY_STRICT,
+} UNumberRoundingPriority;
+#endif // U_FORCE_HIDE_DRAFT_API
+
/**
* An enum declaring how to render units, including currencies. Example outputs when formatting 123 USD and 123
* meters in <em>en-CA</em>:
diff --git a/icu4c/source/test/intltest/numbertest_api.cpp b/icu4c/source/test/intltest/numbertest_api.cpp
index ad2d451..f64c99b 100644
--- a/icu4c/source/test/intltest/numbertest_api.cpp
+++ b/icu4c/source/test/intltest/numbertest_api.cpp
@@ -2708,6 +2708,71 @@
Locale::getEnglish(),
0.0999999,
u"0.10");
+
+ assertFormatDescending(
+ u"FracSig withSignificantDigits RELAXED",
+ u"precision-integer/@#r",
+ u"./@#r",
+ NumberFormatter::with().precision(Precision::maxFraction(0)
+ .withSignificantDigits(1, 2, UNUM_ROUNDING_PRIORITY_RELAXED)),
+ Locale::getEnglish(),
+ u"87,650",
+ u"8,765",
+ u"876",
+ u"88",
+ u"8.8",
+ u"0.88",
+ u"0.088",
+ u"0.0088",
+ u"0");
+
+ assertFormatDescending(
+ u"FracSig withSignificantDigits STRICT",
+ u"precision-integer/@#s",
+ u"./@#",
+ NumberFormatter::with().precision(Precision::maxFraction(0)
+ .withSignificantDigits(1, 2, UNUM_ROUNDING_PRIORITY_STRICT)),
+ Locale::getEnglish(),
+ u"88,000",
+ u"8,800",
+ u"880",
+ u"88",
+ u"9",
+ u"1",
+ u"0",
+ u"0",
+ u"0");
+
+ assertFormatSingle(
+ u"FracSig withSignificantDigits Trailing Zeros RELAXED",
+ u".0/@@@r",
+ u".0/@@@r",
+ NumberFormatter::with().precision(Precision::fixedFraction(1)
+ .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_RELAXED)),
+ Locale::getEnglish(),
+ 1,
+ u"1.00");
+
+ // Trailing zeros are always retained:
+ assertFormatSingle(
+ u"FracSig withSignificantDigits Trailing Zeros STRICT",
+ u".0/@@@s",
+ u".0/@@@s",
+ NumberFormatter::with().precision(Precision::fixedFraction(1)
+ .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT)),
+ Locale::getEnglish(),
+ 1,
+ u"1.00");
+
+ assertFormatSingle(
+ u"FracSig withSignificantDigits at rounding boundary",
+ u"precision-integer/@@@s",
+ u"./@@@s",
+ NumberFormatter::with().precision(Precision::fixedFraction(0)
+ .withSignificantDigits(3, 3, UNUM_ROUNDING_PRIORITY_STRICT)),
+ Locale::getEnglish(),
+ 9.99,
+ u"10.0");
}
void NumberFormatterApiTest::roundingOther() {
diff --git a/icu4c/source/test/intltest/numbertest_skeletons.cpp b/icu4c/source/test/intltest/numbertest_skeletons.cpp
index 07a864a..67c755d 100644
--- a/icu4c/source/test/intltest/numbertest_skeletons.cpp
+++ b/icu4c/source/test/intltest/numbertest_skeletons.cpp
@@ -56,6 +56,10 @@
u".00/@@*",
u".00/@@+",
u".00/@##",
+ u".00/@",
+ u".00/@r",
+ u".00/@@s",
+ u".00/@@#r",
u"precision-increment/3.14",
u"precision-currency-standard",
u"precision-integer rounding-mode-half-up",
@@ -159,13 +163,13 @@
u"@#+",
u"@@x",
u"@@##0",
- u".00/@",
u".00/@@",
u".00/@@x",
u".00/@@#",
u".00/@@#*",
u".00/floor/@@*", // wrong order
u".00/@@#+",
+ u".00/@@@+r",
u".00/floor/@@+", // wrong order
u"precision-increment/français", // non-invariant characters for C++
u"scientific/ee",
@@ -337,7 +341,6 @@
} cases[] = {
{ u".00*", u".00+" },
{ u"@@*", u"@@+" },
- { u".00/@@*", u".00/@@+" },
{ u"scientific/*ee", u"scientific/+ee" },
{ u"integer-width/*00", u"integer-width/+00" },
};
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java b/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
index 3752b1a..90c2fd2 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/FractionPrecision.java
@@ -20,6 +20,36 @@
}
/**
+ * Override maximum fraction digits with maximum significant digits depending on the magnitude
+ * of the number. See UNumberRoundingPriority.
+ *
+ * @param minSignificantDigits
+ * Pad trailing zeros to achieve this minimum number of significant digits.
+ * @param maxSignificantDigits
+ * Round the number to achieve this maximum number of significant digits.
+ * @param priority
+ * How to disambiguate between fraction digits and significant digits.
+ * @return A precision for chaining or passing to the NumberFormatter precision() setter.
+ *
+ * @draft ICU 69
+ */
+ public Precision withSignificantDigits(
+ int minSignificantDigits,
+ int maxSignificantDigits,
+ NumberFormatter.RoundingPriority priority) {
+ if (maxSignificantDigits >= 1 &&
+ maxSignificantDigits >= minSignificantDigits &&
+ maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
+ return constructFractionSignificant(
+ this, minSignificantDigits, maxSignificantDigits, priority);
+ } else {
+ throw new IllegalArgumentException("Significant digits must be between 1 and "
+ + RoundingUtils.MAX_INT_FRAC_SIG
+ + " (inclusive)");
+ }
+ }
+
+ /**
* Ensure that no less than this number of significant digits are retained when rounding according to
* fraction rules.
*
@@ -31,6 +61,9 @@
* This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3",
* not "3.0".
*
+ * <p>
+ * This is equivalent to `withSignificantDigits(1, minSignificantDigits, RELAXED)`.
+ *
* @param minSignificantDigits
* The number of significant figures to guarantee.
* @return A Precision for chaining or passing to the NumberFormatter rounding() setter.
@@ -40,7 +73,8 @@
*/
public Precision withMinDigits(int minSignificantDigits) {
if (minSignificantDigits >= 1 && minSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
- return constructFractionSignificant(this, minSignificantDigits, -1);
+ return constructFractionSignificant(
+ this, 1, minSignificantDigits, NumberFormatter.RoundingPriority.RELAXED);
} else {
throw new IllegalArgumentException("Significant digits must be between 1 and "
+ RoundingUtils.MAX_INT_FRAC_SIG
@@ -60,6 +94,9 @@
* This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2,
* 123.4 would become "120.00".
*
+ * <p>
+ * This is equivalent to `withSignificantDigits(1, maxSignificantDigits, STRICT)`.
+ *
* @param maxSignificantDigits
* Round the number to no more than this number of significant figures.
* @return A Precision for chaining or passing to the NumberFormatter rounding() setter.
@@ -69,7 +106,8 @@
*/
public Precision withMaxDigits(int maxSignificantDigits) {
if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
- return constructFractionSignificant(this, -1, maxSignificantDigits);
+ return constructFractionSignificant(
+ this, 1, maxSignificantDigits, NumberFormatter.RoundingPriority.STRICT);
} else {
throw new IllegalArgumentException("Significant digits must be between 1 and "
+ RoundingUtils.MAX_INT_FRAC_SIG
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java
index e5e1e4d..3d6316d 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberFormatter.java
@@ -69,6 +69,69 @@
private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter();
/**
+ * An enum declaring how to resolve conflicts between maximum fraction digits and maximum
+ * significant digits.
+ *
+ * <p>There are two modes, RELAXED and STRICT:
+ *
+ * <ul>
+ * <li> RELAXED: Relax one of the two constraints (fraction digits or significant digits) in order
+ * to round the number to a higher level of precision.
+ * <li> STRICT: Enforce both constraints, resulting in the number being rounded to a lower
+ * level of precision.
+ * </ul>
+ *
+ * <p>The default settings for compact notation rounding are Max-Fraction = 0 (round to the nearest
+ * integer), Max-Significant = 2 (round to 2 significant digits), and priority RELAXED (choose
+ * the constraint that results in more digits being displayed).
+ *
+ * <p>Conflicting *minimum* fraction and significant digits are always resolved in the direction that
+ * results in more trailing zeros.
+ *
+ * <p>Example 1: Consider the number 3.141, with various different settings:
+ *
+ * <ul>
+ * <li> Max-Fraction = 1: "3.1"
+ * <li> Max-Significant = 3: "3.14"
+ * </ul>
+ *
+ * <p>The rounding priority determines how to resolve the conflict when both Max-Fraction and
+ * Max-Significant are set. With RELAXED, the less-strict setting (the one that causes more digits
+ * to be displayed) will be used; Max-Significant wins. With STRICT, the more-strict setting (the
+ * one that causes fewer digits to be displayed) will be used; Max-Fraction wins.
+ *
+ * <p>Example 2: Consider the number 8317, with various different settings:
+ *
+ * <ul>
+ * <li> Max-Fraction = 1: "8317"
+ * <li> Max-Significant = 3: "8320"
+ * </ul>
+ *
+ * <p>Here, RELAXED favors Max-Fraction and STRICT favors Max-Significant. Note that this larger
+ * number caused the two modes to favor the opposite result.
+ *
+ * @provisional This API might change or be removed in a future release.
+ * @draft ICU 69
+ */
+ public static enum RoundingPriority {
+ /**
+ * Favor greater precision by relaxing one of the rounding constraints.
+ *
+ * @provisional This API might change or be removed in a future release.
+ * @draft ICU 69
+ */
+ RELAXED,
+
+ /**
+ * Favor adherence to all rounding constraints by producing lower precision.
+ *
+ * @provisional This API might change or be removed in a future release.
+ * @draft ICU 69
+ */
+ STRICT,
+ }
+
+ /**
* An enum declaring how to render units, including currencies. Example outputs when formatting 123
* USD and 123 meters in <em>en-CA</em>:
*
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
index 246abea..3da00bf 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberSkeletonImpl.java
@@ -14,6 +14,7 @@
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
+import com.ibm.icu.number.NumberFormatter.RoundingPriority;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.DecimalFormatSymbols;
@@ -1291,20 +1292,14 @@
break;
}
}
- // For the frac-sig option, there must be minSig or maxSig but not both.
- // Valid: @+, @@+, @@@+
- // Valid: @#, @##, @###
- // Invalid: @, @@, @@@
- // Invalid: @@#, @@##, @@@#
if (offset < segment.length()) {
if (isWildcardChar(segment.charAt(offset))) {
+ // @+, @@+, @@@+
maxSig = -1;
offset++;
- } else if (minSig > 1) {
- // @@#, @@##, @@@#
- throw new SkeletonSyntaxException("Invalid digits option for fraction rounder",
- segment);
} else {
+ // @#, @##, @###
+ // @@#, @@##, @@@#
maxSig = minSig;
for (; offset < segment.length(); offset++) {
if (segment.charAt(offset) == '#') {
@@ -1316,18 +1311,43 @@
}
} else {
// @, @@, @@@
- throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+ maxSig = minSig;
}
+ RoundingPriority priority;
if (offset < segment.length()) {
- throw new SkeletonSyntaxException("Invalid digits option for fraction rounder", segment);
+ if (maxSig == -1) {
+ throw new SkeletonSyntaxException(
+ "Invalid digits option: Wildcard character not allowed with the priority annotation", segment);
+ }
+ if (segment.codePointAt(offset) == 'r') {
+ priority = RoundingPriority.RELAXED;
+ offset++;
+ } else if (segment.codePointAt(offset) == 's') {
+ priority = RoundingPriority.STRICT;
+ offset++;
+ } else {
+ assert offset < segment.length();
+ priority = RoundingPriority.RELAXED; // make compiler happy (uninitialized variable)
+ }
+ if (offset < segment.length()) {
+ throw new SkeletonSyntaxException(
+ "Invalid digits option for fraction rounder", segment);
+ }
+ } else if (maxSig == -1) {
+ // withMinDigits
+ maxSig = minSig;
+ minSig = 1;
+ priority = RoundingPriority.RELAXED;
+ } else if (minSig == 1) {
+ // withMaxDigits
+ priority = RoundingPriority.STRICT;
+ } else {
+ throw new SkeletonSyntaxException(
+ "Invalid digits option: Priority annotation required", segment);
}
FractionPrecision oldRounder = (FractionPrecision) macros.precision;
- if (maxSig == -1) {
- macros.precision = oldRounder.withMinDigits(minSig);
- } else {
- macros.precision = oldRounder.withMaxDigits(maxSig);
- }
+ macros.precision = oldRounder.withSignificantDigits(minSig, maxSig, priority);
return true;
}
@@ -1526,10 +1546,11 @@
Precision.FracSigRounderImpl impl = (Precision.FracSigRounderImpl) macros.precision;
BlueprintHelpers.generateFractionStem(impl.minFrac, impl.maxFrac, sb);
sb.append('/');
- if (impl.minSig == -1) {
- BlueprintHelpers.generateDigitsStem(1, impl.maxSig, sb);
+ BlueprintHelpers.generateDigitsStem(impl.minSig, impl.maxSig, sb);
+ if (impl.priority == RoundingPriority.RELAXED) {
+ sb.append('r');
} else {
- BlueprintHelpers.generateDigitsStem(impl.minSig, -1, sb);
+ sb.append('s');
}
} else if (macros.precision instanceof Precision.IncrementRounderImpl) {
Precision.IncrementRounderImpl impl = (Precision.IncrementRounderImpl) macros.precision;
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java b/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
index 0b46cf6..2d0a28a 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/number/Precision.java
@@ -9,6 +9,7 @@
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.MultiplierProducer;
import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.number.NumberFormatter.RoundingPriority;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
@@ -382,7 +383,7 @@
static final SignificantRounderImpl FIXED_SIG_3 = new SignificantRounderImpl(3, 3);
static final SignificantRounderImpl RANGE_SIG_2_3 = new SignificantRounderImpl(2, 3);
- static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 2, -1);
+ static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED);
static final IncrementFiveRounderImpl NICKEL = new IncrementFiveRounderImpl(new BigDecimal("0.05"), 2, 2);
@@ -418,14 +419,16 @@
}
}
- static Precision constructFractionSignificant(FractionPrecision base_, int minSig, int maxSig) {
+ static Precision constructFractionSignificant(
+ FractionPrecision base_, int minSig, int maxSig, RoundingPriority priority) {
assert base_ instanceof FractionRounderImpl;
FractionRounderImpl base = (FractionRounderImpl) base_;
Precision returnValue;
- if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) {
+ if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 &&
+ priority == RoundingPriority.RELAXED) {
returnValue = COMPACT_STRATEGY;
} else {
- returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig);
+ returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority);
}
return returnValue.withMode(base.mathContext);
}
@@ -683,34 +686,37 @@
final int maxFrac;
final int minSig;
final int maxSig;
+ final RoundingPriority priority;
- public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig) {
+ public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority) {
this.minFrac = minFrac;
this.maxFrac = maxFrac;
this.minSig = minSig;
this.maxSig = maxSig;
+ this.priority = priority;
}
@Override
public void apply(DecimalQuantity value) {
- int displayMag = getDisplayMagnitudeFraction(minFrac);
- int roundingMag = getRoundingMagnitudeFraction(maxFrac);
- if (minSig == -1) {
- // Max Sig override
- int candidate = getRoundingMagnitudeSignificant(value, maxSig);
- roundingMag = Math.max(roundingMag, candidate);
+ int roundingMag1 = getRoundingMagnitudeFraction(maxFrac);
+ int roundingMag2 = getRoundingMagnitudeSignificant(value, maxSig);
+ int roundingMag;
+ if (priority == RoundingPriority.RELAXED) {
+ roundingMag = Math.min(roundingMag1, roundingMag2);
} else {
- // Min Sig override
- int candidate = getDisplayMagnitudeSignificant(value, minSig);
- roundingMag = Math.min(roundingMag, candidate);
+ roundingMag = Math.max(roundingMag1, roundingMag2);
}
value.roundToMagnitude(roundingMag, mathContext);
+
+ int displayMag1 = getDisplayMagnitudeFraction(minFrac);
+ int displayMag2 = getDisplayMagnitudeSignificant(value, minSig);
+ int displayMag = Math.min(displayMag1, displayMag2);
value.setMinFraction(Math.max(0, -displayMag));
}
@Override
FracSigRounderImpl createCopy() {
- FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig);
+ FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority);
copy.mathContext = mathContext;
return copy;
}
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 7f1f75d..d2414df 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
@@ -37,6 +37,7 @@
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
+import com.ibm.icu.number.NumberFormatter.RoundingPriority;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.number.Precision;
@@ -2518,6 +2519,71 @@
ULocale.ENGLISH,
0.0999999,
"0.10");
+
+ assertFormatDescending(
+ "FracSig withSignificantDigits RELAXED",
+ "precision-integer/@#r",
+ "./@#r",
+ NumberFormatter.with().precision(Precision.maxFraction(0)
+ .withSignificantDigits(1, 2, RoundingPriority.RELAXED)),
+ ULocale.ENGLISH,
+ "87,650",
+ "8,765",
+ "876",
+ "88",
+ "8.8",
+ "0.88",
+ "0.088",
+ "0.0088",
+ "0");
+
+ assertFormatDescending(
+ "FracSig withSignificantDigits STRICT",
+ "precision-integer/@#s",
+ "./@#",
+ NumberFormatter.with().precision(Precision.maxFraction(0)
+ .withSignificantDigits(1, 2, RoundingPriority.STRICT)),
+ ULocale.ENGLISH,
+ "88,000",
+ "8,800",
+ "880",
+ "88",
+ "9",
+ "1",
+ "0",
+ "0",
+ "0");
+
+ assertFormatSingle(
+ "FracSig withSignificantDigits Trailing Zeros RELAXED",
+ ".0/@@@r",
+ ".0/@@@r",
+ NumberFormatter.with().precision(Precision.fixedFraction(1)
+ .withSignificantDigits(3, 3, RoundingPriority.RELAXED)),
+ ULocale.ENGLISH,
+ 1,
+ "1.00");
+
+ // Trailing zeros are always retained:
+ assertFormatSingle(
+ "FracSig withSignificantDigits Trailing Zeros STRICT",
+ ".0/@@@s",
+ ".0/@@@s",
+ NumberFormatter.with().precision(Precision.fixedFraction(1)
+ .withSignificantDigits(3, 3, RoundingPriority.STRICT)),
+ ULocale.ENGLISH,
+ 1,
+ "1.00");
+
+ assertFormatSingle(
+ "FracSig withSignificantDigits at rounding boundary",
+ "precision-integer/@@@s",
+ "./@@@s",
+ NumberFormatter.with().precision(Precision.fixedFraction(0)
+ .withSignificantDigits(3, 3, RoundingPriority.STRICT)),
+ ULocale.ENGLISH,
+ 9.99,
+ "10.0");
}
@Test
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java
index bda900b..7f9645f 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberSkeletonTest.java
@@ -40,6 +40,10 @@
".00/@@*",
".00/@@+",
".00/@##",
+ ".00/@",
+ ".00/@r",
+ ".00/@@s",
+ ".00/@@#r",
"precision-increment/3.14",
"precision-currency-standard",
"precision-integer rounding-mode-half-up",
@@ -142,13 +146,13 @@
"@#+",
"@@x",
"@@##0",
- ".00/@",
".00/@@",
".00/@@x",
".00/@@#",
".00/@@#*",
".00/floor/@@*", // wrong order
".00/@@#+",
+ ".00/@@@+r",
".00/floor/@@+", // wrong order
"precision-increment/français", // non-invariant characters for C++
"scientific/ee",
@@ -322,7 +326,6 @@
String[][] cases = {
{ ".00*", ".00+" },
{ "@@*", "@@+" },
- { ".00/@@*", ".00/@@+" },
{ "scientific/*ee", "scientific/+ee" },
{ "integer-width/*00", "integer-width/+00" },
};