ICU-21358 Use sign position to format approximate numbers

See #1635
This commit is contained in:
Shane F. Carr 2021-03-17 17:45:58 +00:00
parent 49dda34fb1
commit 4e01fba906
38 changed files with 542 additions and 105 deletions

View file

@ -92,6 +92,7 @@ static const char *gNumberElementKeys[DecimalFormatSymbols::kFormatSymbolCount]
NULL, /* eight digit - get it from the numbering system */
NULL, /* nine digit - get it from the numbering system */
"superscriptingExponent", /* Multiplication (x) symbol for exponents */
"approximatelySign" /* Approximately sign symbol */
};
// -------------------------------------
@ -508,6 +509,7 @@ DecimalFormatSymbols::initialize() {
fSymbols[kSignificantDigitSymbol] = (UChar)0x0040; // '@' significant digit
fSymbols[kMonetaryGroupingSeparatorSymbol].remove(); //
fSymbols[kExponentMultiplicationSymbol] = (UChar)0xd7; // 'x' multiplication symbol for exponents
fSymbols[kApproximatelySignSymbol] = u'~'; // '~' approximately sign
fIsCustomCurrencySymbol = FALSE;
fIsCustomIntlCurrencySymbol = FALSE;
fCodePointZero = 0x30;

View file

@ -134,6 +134,9 @@ Field AffixUtils::getFieldForType(AffixPatternType type) {
return {UFIELD_CATEGORY_NUMBER, UNUM_SIGN_FIELD};
case TYPE_PLUS_SIGN:
return {UFIELD_CATEGORY_NUMBER, UNUM_SIGN_FIELD};
case TYPE_APPROXIMATELY_SIGN:
// TODO: Introduce a new field for the approximately sign?
return {UFIELD_CATEGORY_NUMBER, UNUM_SIGN_FIELD};
case TYPE_PERCENT:
return {UFIELD_CATEGORY_NUMBER, UNUM_PERCENT_FIELD};
case TYPE_PERMILLE:
@ -295,6 +298,8 @@ AffixTag AffixUtils::nextToken(AffixTag tag, const UnicodeString &patternString,
return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
case u'+':
return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
case u'~':
return makeTag(offset + count, TYPE_APPROXIMATELY_SIGN, STATE_BASE, 0);
case u'%':
return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
case u'':

View file

@ -289,6 +289,11 @@ void DecimalQuantity::adjustExponent(int delta) {
exponent = exponent + delta;
}
void DecimalQuantity::resetExponent() {
adjustMagnitude(exponent);
exponent = 0;
}
bool DecimalQuantity::hasIntegerValue() const {
return scale >= 0;
}

View file

@ -166,6 +166,11 @@ class U_I18N_API DecimalQuantity : public IFixedDecimal, public UMemory {
*/
void adjustExponent(int32_t delta);
/**
* Resets the DecimalQuantity to the value before adjustMagnitude and adjustExponent.
*/
void resetExponent();
/**
* @return Whether the value represented by this {@link DecimalQuantity} is
* zero, infinity, or NaN.

View file

@ -356,7 +356,7 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe,
macros.affixProvider != nullptr ? macros.affixProvider
: static_cast<const AffixPatternProvider*>(fPatternInfo.getAlias()),
kUndefinedField);
patternModifier->setPatternAttributes(fMicros.sign, isPermille);
patternModifier->setPatternAttributes(fMicros.sign, isPermille, macros.approximately);
if (patternModifier->needsPlurals()) {
patternModifier->setSymbols(
fMicros.symbols,

View file

@ -33,6 +33,12 @@ class NumberFormatterImpl : public UMemory {
*/
NumberFormatterImpl(const MacroProps &macros, UErrorCode &status);
/**
* Default constructor; leaves the NumberFormatterImpl in an undefined state.
* Takes an error code to prevent the method from being called accidentally.
*/
NumberFormatterImpl(UErrorCode &) {}
/**
* Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
*/

View file

@ -28,9 +28,13 @@ void MutablePatternModifier::setPatternInfo(const AffixPatternProvider* patternI
fField = field;
}
void MutablePatternModifier::setPatternAttributes(UNumberSignDisplay signDisplay, bool perMille) {
void MutablePatternModifier::setPatternAttributes(
UNumberSignDisplay signDisplay,
bool perMille,
bool approximately) {
fSignDisplay = signDisplay;
fPerMilleReplacesPercent = perMille;
fApproximately = approximately;
}
void MutablePatternModifier::setSymbols(const DecimalFormatSymbols* symbols,
@ -277,6 +281,7 @@ void MutablePatternModifier::prepareAffix(bool isPrefix) {
*fPatternInfo,
isPrefix,
PatternStringUtils::resolveSignDisplay(fSignDisplay, fSignum),
fApproximately,
fPlural,
fPerMilleReplacesPercent,
currentAffix);
@ -289,6 +294,8 @@ UnicodeString MutablePatternModifier::getSymbol(AffixPatternType type) const {
return fSymbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kMinusSignSymbol);
case AffixPatternType::TYPE_PLUS_SIGN:
return fSymbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kPlusSignSymbol);
case AffixPatternType::TYPE_APPROXIMATELY_SIGN:
return fSymbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kApproximatelySignSymbol);
case AffixPatternType::TYPE_PERCENT:
return fSymbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kPercentSymbol);
case AffixPatternType::TYPE_PERMILLE:

View file

@ -116,8 +116,10 @@ class U_I18N_API MutablePatternModifier
* Whether to force a plus sign on positive numbers.
* @param perMille
* Whether to substitute the percent sign in the pattern with a permille sign.
* @param approximately
* Whether to prepend approximately to the sign
*/
void setPatternAttributes(UNumberSignDisplay signDisplay, bool perMille);
void setPatternAttributes(UNumberSignDisplay signDisplay, bool perMille, bool approximately);
/**
* Sets locale-specific details that affect the symbols substituted into the pattern string affixes.
@ -204,6 +206,7 @@ class U_I18N_API MutablePatternModifier
Field fField;
UNumberSignDisplay fSignDisplay;
bool fPerMilleReplacesPercent;
bool fApproximately;
// Symbol details (initialized in setSymbols)
const DecimalFormatSymbols *fSymbols;

View file

@ -869,6 +869,7 @@ PatternStringUtils::convertLocalized(const UnicodeString& input, const DecimalFo
UnicodeString table[LEN][2];
int standIdx = toLocalized ? 0 : 1;
int localIdx = toLocalized ? 1 : 0;
// TODO: Add approximately sign here?
table[0][standIdx] = u"%";
table[0][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kPercentSymbol);
table[1][standIdx] = u"";
@ -1001,6 +1002,7 @@ PatternStringUtils::convertLocalized(const UnicodeString& input, const DecimalFo
void PatternStringUtils::patternInfoToStringBuilder(const AffixPatternProvider& patternInfo, bool isPrefix,
PatternSignType patternSignType,
bool approximately,
StandardPlural::Form plural,
bool perMilleReplacesPercent, UnicodeString& output) {
@ -1012,7 +1014,7 @@ void PatternStringUtils::patternInfoToStringBuilder(const AffixPatternProvider&
// (If not, we will use the positive subpattern.)
bool useNegativeAffixPattern = patternInfo.hasNegativeSubpattern()
&& (patternSignType == PATTERN_SIGN_TYPE_NEG
|| (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
|| (patternInfo.negativeHasMinusSign() && (plusReplacesMinusSign || approximately)));
// Resolve the flags for the affix pattern.
int flags = 0;
@ -1034,10 +1036,24 @@ void PatternStringUtils::patternInfoToStringBuilder(const AffixPatternProvider&
} else if (patternSignType == PATTERN_SIGN_TYPE_NEG) {
prependSign = true;
} else {
prependSign = plusReplacesMinusSign;
prependSign = plusReplacesMinusSign || approximately;
}
// Compute the length of the affix pattern.
// What symbols should take the place of the sign placeholder?
const char16_t* signSymbols = u"-";
if (approximately) {
if (plusReplacesMinusSign) {
signSymbols = u"~+";
} else if (patternSignType == PATTERN_SIGN_TYPE_NEG) {
signSymbols = u"~-";
} else {
signSymbols = u"~";
}
} else if (plusReplacesMinusSign) {
signSymbols = u"+";
}
// Compute the number of tokens in the affix pattern (signSymbols is considered one token).
int length = patternInfo.length(flags) + (prependSign ? 1 : 0);
// Finally, set the result into the StringBuilder.
@ -1051,8 +1067,13 @@ void PatternStringUtils::patternInfoToStringBuilder(const AffixPatternProvider&
} else {
candidate = patternInfo.charAt(flags, index);
}
if (plusReplacesMinusSign && candidate == u'-') {
candidate = u'+';
if (candidate == u'-') {
if (u_strlen(signSymbols) == 1) {
candidate = signSymbols[0];
} else {
output.append(signSymbols[0]);
candidate = signSymbols[1];
}
}
if (perMilleReplacesPercent && candidate == u'%') {
candidate = u'';

View file

@ -308,6 +308,7 @@ class U_I18N_API PatternStringUtils {
*/
static void patternInfoToStringBuilder(const AffixPatternProvider& patternInfo, bool isPrefix,
PatternSignType patternSignType,
bool approximately,
StandardPlural::Form plural, bool perMilleReplacesPercent,
UnicodeString& output);

View file

@ -52,7 +52,7 @@ class ScientificHandler : public UMemory, public MicroPropsGenerator, public Mul
int32_t getMultiplier(int32_t magnitude) const U_OVERRIDE;
private:
const Notation::ScientificSettings& fSettings;
const Notation::ScientificSettings fSettings;
const DecimalFormatSymbols *fSymbols;
const MicroPropsGenerator *fParent;

View file

@ -62,26 +62,29 @@ enum AffixPatternType {
// Represents a plus sign symbol '+'.
TYPE_PLUS_SIGN = -2,
// Represents an approximately sign symbol '~'.
TYPE_APPROXIMATELY_SIGN = -3,
// Represents a percent sign symbol '%'.
TYPE_PERCENT = -3,
TYPE_PERCENT = -4,
// Represents a permille sign symbol '‰'.
TYPE_PERMILLE = -4,
TYPE_PERMILLE = -5,
// Represents a single currency symbol '¤'.
TYPE_CURRENCY_SINGLE = -5,
TYPE_CURRENCY_SINGLE = -6,
// Represents a double currency symbol '¤¤'.
TYPE_CURRENCY_DOUBLE = -6,
TYPE_CURRENCY_DOUBLE = -7,
// Represents a triple currency symbol '¤¤¤'.
TYPE_CURRENCY_TRIPLE = -7,
TYPE_CURRENCY_TRIPLE = -8,
// Represents a quadruple currency symbol '¤¤¤¤'.
TYPE_CURRENCY_QUAD = -8,
TYPE_CURRENCY_QUAD = -9,
// Represents a quintuple currency symbol '¤¤¤¤¤'.
TYPE_CURRENCY_QUINT = -9,
TYPE_CURRENCY_QUINT = -10,
// Represents a sequence of six or more currency symbols.
TYPE_CURRENCY_OVERFLOW = -15

View file

@ -294,18 +294,20 @@ void AffixMatcherWarehouse::createAffixMatchers(const AffixPatternProvider& patt
}
// Generate Prefix
// TODO: Handle approximately sign?
bool hasPrefix = false;
PatternStringUtils::patternInfoToStringBuilder(
patternInfo, true, type, StandardPlural::OTHER, false, sb);
patternInfo, true, type, false, StandardPlural::OTHER, false, sb);
fAffixPatternMatchers[numAffixPatternMatchers] = AffixPatternMatcher::fromAffixPattern(
sb, *fTokenWarehouse, parseFlags, &hasPrefix, status);
AffixPatternMatcher* prefix = hasPrefix ? &fAffixPatternMatchers[numAffixPatternMatchers++]
: nullptr;
// Generate Suffix
// TODO: Handle approximately sign?
bool hasSuffix = false;
PatternStringUtils::patternInfoToStringBuilder(
patternInfo, false, type, StandardPlural::OTHER, false, sb);
patternInfo, false, type, false, StandardPlural::OTHER, false, sb);
fAffixPatternMatchers[numAffixPatternMatchers] = AffixPatternMatcher::fromAffixPattern(
sb, *fTokenWarehouse, parseFlags, &hasSuffix, status);
AffixPatternMatcher* suffix = hasSuffix ? &fAffixPatternMatchers[numAffixPatternMatchers++]

View file

@ -30,7 +30,8 @@ constexpr int8_t identity2d(UNumberRangeIdentityFallback a, UNumberRangeIdentity
struct NumberRangeData {
SimpleFormatter rangePattern;
SimpleFormatter approximatelyPattern;
// Note: approximatelyPattern is unused since ICU 69.
// SimpleFormatter approximatelyPattern;
};
class NumberRangeDataSink : public ResourceSink {
@ -46,12 +47,16 @@ class NumberRangeDataSink : public ResourceSink {
continue; // have already seen this pattern
}
fData.rangePattern = {value.getUnicodeString(status), status};
} else if (uprv_strcmp(key, "approximately") == 0) {
}
/*
// Note: approximatelyPattern is unused since ICU 69.
else if (uprv_strcmp(key, "approximately") == 0) {
if (hasApproxData()) {
continue; // have already seen this pattern
}
fData.approximatelyPattern = {value.getUnicodeString(status), status};
}
*/
}
}
@ -59,21 +64,26 @@ class NumberRangeDataSink : public ResourceSink {
return fData.rangePattern.getArgumentLimit() != 0;
}
/*
// Note: approximatelyPattern is unused since ICU 69.
bool hasApproxData() {
return fData.approximatelyPattern.getArgumentLimit() != 0;
}
*/
bool isComplete() {
return hasRangeData() && hasApproxData();
return hasRangeData() /* && hasApproxData() */;
}
void fillInDefaults(UErrorCode& status) {
if (!hasRangeData()) {
fData.rangePattern = {u"{0}{1}", status};
}
/*
if (!hasApproxData()) {
fData.approximatelyPattern = {u"~{0}", status};
}
*/
}
private:
@ -116,7 +126,8 @@ NumberRangeFormatterImpl::NumberRangeFormatterImpl(const RangeMacroProps& macros
formatterImpl2(macros.formatter2.fMacros, status),
fSameFormatters(macros.singleFormatter),
fCollapse(macros.collapse),
fIdentityFallback(macros.identityFallback) {
fIdentityFallback(macros.identityFallback),
fApproximatelyFormatter(status) {
const char* nsName = formatterImpl1.getRawMicroProps().nsName;
if (uprv_strcmp(nsName, formatterImpl2.getRawMicroProps().nsName) != 0) {
@ -128,7 +139,16 @@ NumberRangeFormatterImpl::NumberRangeFormatterImpl(const RangeMacroProps& macros
getNumberRangeData(macros.locale.getName(), nsName, data, status);
if (U_FAILURE(status)) { return; }
fRangeFormatter = data.rangePattern;
fApproximatelyModifier = {data.approximatelyPattern, kUndefinedField, false};
if (fSameFormatters && (
fIdentityFallback == UNUM_IDENTITY_FALLBACK_APPROXIMATELY ||
fIdentityFallback == UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE)) {
MacroProps approximatelyMacros(macros.formatter1.fMacros);
approximatelyMacros.approximately = true;
// Use in-place construction because NumberFormatterImpl has internal self-pointers
fApproximatelyFormatter.~NumberFormatterImpl();
new (&fApproximatelyFormatter) NumberFormatterImpl(approximatelyMacros, status);
}
// TODO: Get locale from PluralRules instead?
fPluralRanges = StandardPluralRanges::forLocale(macros.locale, status);
@ -232,12 +252,14 @@ void NumberRangeFormatterImpl::formatApproximately (UFormattedNumberRangeData& d
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
if (fSameFormatters) {
int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.getStringRef(), 0, status);
// HEURISTIC: Desired modifier order: inner, middle, approximately, outer.
length += micros1.modInner->apply(data.getStringRef(), 0, length, status);
length += micros1.modMiddle->apply(data.getStringRef(), 0, length, status);
length += fApproximatelyModifier.apply(data.getStringRef(), 0, length, status);
micros1.modOuter->apply(data.getStringRef(), 0, length, status);
// Re-format using the approximately formatter:
MicroProps microsAppx;
data.quantity1.resetExponent();
fApproximatelyFormatter.preProcess(data.quantity1, microsAppx, status);
int32_t length = NumberFormatterImpl::writeNumber(microsAppx, data.quantity1, data.getStringRef(), 0, status);
length += microsAppx.modInner->apply(data.getStringRef(), 0, length, status);
length += microsAppx.modMiddle->apply(data.getStringRef(), 0, length, status);
microsAppx.modOuter->apply(data.getStringRef(), 0, length, status);
} else {
formatRange(data, micros1, micros2, status);
}

View file

@ -56,7 +56,7 @@ class NumberRangeFormatterImpl : public UMemory {
UNumberRangeIdentityFallback fIdentityFallback;
SimpleFormatter fRangeFormatter;
SimpleModifier fApproximatelyModifier;
NumberFormatterImpl fApproximatelyFormatter;
StandardPluralRanges fPluralRanges;

View file

@ -169,8 +169,14 @@ public:
* @stable ICU 54
*/
kExponentMultiplicationSymbol,
#ifndef U_HIDE_INTERNAL_API
/** Approximately sign.
* @internal
*/
kApproximatelySignSymbol,
#endif
/** count symbol constants */
kFormatSymbolCount = kNineDigitSymbol + 2
kFormatSymbolCount = kExponentMultiplicationSymbol + 2
};
/**

View file

@ -1517,6 +1517,9 @@ struct U_I18N_API MacroProps : public UMemory {
/** @internal */
UNumberSignDisplay sign = UNUM_SIGN_COUNT;
/** @internal */
bool approximately = false;
/** @internal */
UNumberDecimalSeparatorDisplay decimal = UNUM_DECIMAL_SEPARATOR_COUNT;

View file

@ -1427,12 +1427,19 @@ typedef enum UNumberFormatSymbol {
*/
UNUM_EXPONENT_MULTIPLICATION_SYMBOL = 27,
#ifndef U_HIDE_INTERNAL_API
/** Approximately sign.
* @internal
*/
UNUM_APPROXIMATELY_SIGN_SYMBOL = 28,
#endif
#ifndef U_HIDE_DEPRECATED_API
/**
* One more than the highest normal UNumberFormatSymbol value.
* @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
*/
UNUM_FORMAT_SYMBOL_COUNT = 28
UNUM_FORMAT_SYMBOL_COUNT = 29
#endif /* U_HIDE_DEPRECATED_API */
} UNumberFormatSymbol;

View file

@ -315,11 +315,13 @@ class NumberRangeFormatterTest : public IntlTestWithFieldPosition {
void testCopyMove();
void toObject();
void testGetDecimalNumbers();
void test21358_SignPosition();
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);
private:
CurrencyUnit USD;
CurrencyUnit CHF;
CurrencyUnit GBP;
CurrencyUnit PTE;

View file

@ -24,6 +24,8 @@ class DefaultSymbolProvider : public SymbolProvider {
return u"";
case TYPE_PLUS_SIGN:
return fSymbols.getConstSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kPlusSignSymbol);
case TYPE_APPROXIMATELY_SIGN:
return u"";
case TYPE_PERCENT:
return fSymbols.getConstSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kPercentSymbol);
case TYPE_PERMILLE:
@ -93,6 +95,7 @@ void AffixUtilsTest::testUnescape() {
{u"-!", false, 2, u"!"},
{u"+", false, 1, u"\u061C+"},
{u"+!", false, 2, u"\u061C+!"},
{u"~", false, 1, u""},
{u"", false, 1, u"؉"},
{u"‰!", false, 2, u"؉!"},
{u"-x", false, 2, u"x"},
@ -209,7 +212,7 @@ void AffixUtilsTest::testUnescapeWithSymbolProvider() {
{u"", u""},
{u"-", u"1"},
{u"'-'", u"-"},
{u"- + % ‰ ¤ ¤¤ ¤¤¤ ¤¤¤¤ ¤¤¤¤¤", u"1 2 3 4 5 6 7 8 9"},
{u"- + ~ % ‰ ¤ ¤¤ ¤¤¤ ¤¤¤¤ ¤¤¤¤¤", u"1 2 3 4 5 6 7 8 9 10"},
{u"'¤¤¤¤¤¤'", u"¤¤¤¤¤¤"},
{u"¤¤¤¤¤¤", u"\uFFFD"}
};
@ -232,7 +235,7 @@ void AffixUtilsTest::testUnescapeWithSymbolProvider() {
sb.clear();
sb.append(u"abcdefg", kUndefinedField, status);
assertSuccess("Spot 2", status);
AffixUtils::unescape(u"-+%", sb, 4, provider, kUndefinedField, status);
AffixUtils::unescape(u"-+~", sb, 4, provider, kUndefinedField, status);
assertSuccess("Spot 3", status);
assertEquals(u"Symbol provider into middle", u"abcd123efg", sb.toUnicodeString());
}

View file

@ -2149,6 +2149,26 @@ void NumberFormatterApiTest::unitCurrency() {
Locale("lu"),
123.12,
u"123,12 CN¥");
// de-CH has currency pattern "¤ #,##0.00;¤-#,##0.00"
assertFormatSingle(
u"Sign position on negative number with pattern spacing",
u"currency/RON",
u"currency/RON",
NumberFormatter::with().unit(RON),
Locale("de-CH"),
-123.12,
u"RON-123.12");
// TODO(CLDR-13044): Move the sign to the inside of the number
assertFormatSingle(
u"Sign position on negative number with currency spacing",
u"currency/RON",
u"currency/RON",
NumberFormatter::with().unit(RON),
Locale("en"),
-123.12,
u"-RON 123.12");
}
void NumberFormatterApiTest::runUnitInflectionsTestCases(UnlocalizedNumberFormatter unf,

View file

@ -27,7 +27,7 @@ void PatternModifierTest::testBasic() {
PatternParser::parseToPatternInfo(u"a0b", patternInfo, status);
assertSuccess("Spot 1", status);
mod.setPatternInfo(&patternInfo, kUndefinedField);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false, false);
DecimalFormatSymbols symbols(Locale::getEnglish(), status);
mod.setSymbols(&symbols, {u"USD", status}, UNUM_UNIT_WIDTH_SHORT, nullptr, status);
if (!assertSuccess("Spot 2", status, true)) {
@ -37,7 +37,7 @@ void PatternModifierTest::testBasic() {
mod.setNumberProperties(SIGNUM_POS, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b", u"a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false);
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false, false);
assertEquals("Pattern a0b", u"+a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setNumberProperties(SIGNUM_NEG_ZERO, StandardPlural::Form::COUNT);
@ -46,26 +46,39 @@ void PatternModifierTest::testBasic() {
mod.setNumberProperties(SIGNUM_POS_ZERO, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b", u"+a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_EXCEPT_ZERO, false);
mod.setPatternAttributes(UNUM_SIGN_EXCEPT_ZERO, false, false);
assertEquals("Pattern a0b", u"a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setNumberProperties(SIGNUM_NEG, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b", u"-a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_NEVER, false);
mod.setPatternAttributes(UNUM_SIGN_NEVER, false, false);
assertEquals("Pattern a0b", u"a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
assertSuccess("Spot 3", status);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false, true);
mod.setNumberProperties(SIGNUM_POS, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b", u"~a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setNumberProperties(SIGNUM_NEG, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b", u"~-a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false, true);
mod.setNumberProperties(SIGNUM_POS, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b", u"~+a", getPrefix(mod, status));
assertEquals("Pattern a0b", u"b", getSuffix(mod, status));
assertSuccess("Spot 3.5", status);
ParsedPatternInfo patternInfo2;
PatternParser::parseToPatternInfo(u"a0b;c-0d", patternInfo2, status);
assertSuccess("Spot 4", status);
mod.setPatternInfo(&patternInfo2, kUndefinedField);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false, false);
mod.setNumberProperties(SIGNUM_POS, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b;c-0d", u"a", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"b", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false);
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false, false);
assertEquals("Pattern a0b;c-0d", u"c+", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"d", getSuffix(mod, status));
mod.setNumberProperties(SIGNUM_NEG_ZERO, StandardPlural::Form::COUNT);
@ -74,16 +87,29 @@ void PatternModifierTest::testBasic() {
mod.setNumberProperties(SIGNUM_POS_ZERO, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b;c-0d", u"c+", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"d", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_EXCEPT_ZERO, false);
mod.setPatternAttributes(UNUM_SIGN_EXCEPT_ZERO, false, false);
assertEquals("Pattern a0b;c-0d", u"a", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"b", getSuffix(mod, status));
mod.setNumberProperties(SIGNUM_NEG, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b;c-0d", u"c-", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"d", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_NEVER, false);
mod.setPatternAttributes(UNUM_SIGN_NEVER, false, false);
assertEquals("Pattern a0b;c-0d", u"a", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"b", getSuffix(mod, status));
assertSuccess("Spot 5", status);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false, true);
mod.setNumberProperties(SIGNUM_POS, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b;c-0d", u"c~", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"d", getSuffix(mod, status));
mod.setNumberProperties(SIGNUM_NEG, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b;c-0d", u"c~-", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"d", getSuffix(mod, status));
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false, true);
mod.setNumberProperties(SIGNUM_POS, StandardPlural::Form::COUNT);
assertEquals("Pattern a0b;c-0d", u"c~+", getPrefix(mod, status));
assertEquals("Pattern a0b;c-0d", u"d", getSuffix(mod, status));
assertSuccess("Spot 5.5", status);
}
void PatternModifierTest::testPatternWithNoPlaceholder() {
@ -93,7 +119,7 @@ void PatternModifierTest::testPatternWithNoPlaceholder() {
PatternParser::parseToPatternInfo(u"abc", patternInfo, status);
assertSuccess("Spot 1", status);
mod.setPatternInfo(&patternInfo, kUndefinedField);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false, false);
DecimalFormatSymbols symbols(Locale::getEnglish(), status);
mod.setSymbols(&symbols, {u"USD", status}, UNUM_UNIT_WIDTH_SHORT, nullptr, status);
if (!assertSuccess("Spot 2", status, true)) {
@ -135,7 +161,7 @@ void PatternModifierTest::testMutableEqualsImmutable() {
PatternParser::parseToPatternInfo("a0b;c-0d", patternInfo, status);
assertSuccess("Spot 1", status);
mod.setPatternInfo(&patternInfo, kUndefinedField);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false);
mod.setPatternAttributes(UNUM_SIGN_AUTO, false, false);
DecimalFormatSymbols symbols(Locale::getEnglish(), status);
mod.setSymbols(&symbols, {u"USD", status}, UNUM_UNIT_WIDTH_SHORT, nullptr, status);
assertSuccess("Spot 2", status);
@ -160,7 +186,7 @@ void PatternModifierTest::testMutableEqualsImmutable() {
FormattedStringBuilder nsb3;
MicroProps micros3;
mod.addToChain(&micros3);
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false);
mod.setPatternAttributes(UNUM_SIGN_ALWAYS, false, false);
mod.processQuantity(fq, micros3, status);
micros3.modMiddle->apply(nsb3, 0, 0, status);
assertSuccess("Spot 5", status);

View file

@ -21,6 +21,7 @@ NumberRangeFormatterTest::NumberRangeFormatterTest()
NumberRangeFormatterTest::NumberRangeFormatterTest(UErrorCode& status)
: USD(u"USD", status),
CHF(u"CHF", status),
GBP(u"GBP", status),
PTE(u"PTE", status) {
@ -52,6 +53,7 @@ void NumberRangeFormatterTest::runIndexedTest(int32_t index, UBool exec, const c
TESTCASE_AUTO(testCopyMove);
TESTCASE_AUTO(toObject);
TESTCASE_AUTO(testGetDecimalNumbers);
TESTCASE_AUTO(test21358_SignPosition);
TESTCASE_AUTO_END;
}
@ -135,14 +137,14 @@ void NumberRangeFormatterTest::testBasic() {
.numberFormatterBoth(NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)),
Locale("fr-FR"),
u"15\u00A0degrés Fahrenheit",
u"5\u00A0degrés Fahrenheit",
u"5\u00A0degrés Fahrenheit",
u"5\u00A0degrés Fahrenheit",
u"5\u00A0degrés Fahrenheit",
u"03\u00A0degrés Fahrenheit",
u"0\u00A0degré Fahrenheit",
u"0\u00A0degré Fahrenheit",
u"33\u202F000\u00A0degrés Fahrenheit",
u"3\u202F0005\u202F000\u00A0degrés Fahrenheit",
u"4\u202F9995\u202F001\u00A0degrés Fahrenheit",
u"5\u202F000\u00A0degrés Fahrenheit",
u"5\u202F000\u00A0degrés Fahrenheit",
u"5\u202F0005\u202F000\u202F000\u00A0degrés Fahrenheit");
assertFormatRange(
@ -150,14 +152,14 @@ void NumberRangeFormatterTest::testBasic() {
NumberRangeFormatter::with(),
Locale("ja"),
u"15",
u" 5",
u" 5",
u"5",
u"5",
u"03",
u" 0",
u"0",
u"33,000",
u"3,0005,000",
u"4,9995,001",
u" 5,000",
u"5,000",
u"5,0005,000,000");
assertFormatRange(
@ -905,6 +907,71 @@ void NumberRangeFormatterTest::testGetDecimalNumbers() {
}
}
void NumberRangeFormatterTest::test21358_SignPosition() {
IcuTestErrorCode status(*this, "test21358_SignPosition");
// de-CH has currency pattern "¤ #,##0.00;¤-#,##0.00"
assertFormatRange(
u"Approximately sign position with spacing from pattern",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(CHF)),
Locale("de-CH"),
u"CHF 1.005.00",
u"CHF≈5.00",
u"CHF≈5.00",
u"CHF 0.003.00",
u"CHF≈0.00",
u"CHF 3.003000.00",
u"CHF 3000.005000.00",
u"CHF 4999.005001.00",
u"CHF≈5000.00",
u"CHF 5000.005000000.00");
// TODO(CLDR-13044): Move the sign to the inside of the number
assertFormatRange(
u"Approximately sign position with currency spacing",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(CHF)),
Locale("en-US"),
u"CHF 1.005.00",
u"~CHF 5.00",
u"~CHF 5.00",
u"CHF 0.003.00",
u"~CHF 0.00",
u"CHF 3.003,000.00",
u"CHF 3,000.005,000.00",
u"CHF 4,999.005,001.00",
u"~CHF 5,000.00",
u"CHF 5,000.005,000,000.00");
{
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter::withLocale("de-CH");
UnicodeString actual = lnrf.formatFormattableRange(-2, 3, status).toString(status);
assertEquals("Negative to positive range", u"-2 3", actual);
}
{
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter::withLocale("de-CH")
.numberFormatterBoth(NumberFormatter::forSkeleton(u"%", status));
UnicodeString actual = lnrf.formatFormattableRange(-2, 3, status).toString(status);
assertEquals("Negative to positive percent", u"-2% 3%", actual);
}
{
// TODO(CLDR-14111): Add spacing between range separator and sign
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter::withLocale("de-CH");
UnicodeString actual = lnrf.formatFormattableRange(2, -3, status).toString(status);
assertEquals("Positive to negative range", u"2-3", actual);
}
{
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter::withLocale("de-CH")
.numberFormatterBoth(NumberFormatter::forSkeleton(u"%", status));
UnicodeString actual = lnrf.formatFormattableRange(2, -3, status).toString(status);
assertEquals("Positive to negative percent", u"2% -3%", actual);
}
}
void NumberRangeFormatterTest::assertFormatRange(
const char16_t* message,
const UnlocalizedNumberRangeFormatter& f,

View file

@ -82,26 +82,29 @@ public class AffixUtils {
/** Represents a plus sign symbol '+'. */
public static final int TYPE_PLUS_SIGN = -2;
// Represents an approximately sign symbol '~'.
public static final int TYPE_APPROXIMATELY_SIGN = -3;
/** Represents a percent sign symbol '%'. */
public static final int TYPE_PERCENT = -3;
public static final int TYPE_PERCENT = -4;
/** Represents a permille sign symbol '‰'. */
public static final int TYPE_PERMILLE = -4;
public static final int TYPE_PERMILLE = -5;
/** Represents a single currency symbol '¤'. */
public static final int TYPE_CURRENCY_SINGLE = -5;
public static final int TYPE_CURRENCY_SINGLE = -6;
/** Represents a double currency symbol '¤¤'. */
public static final int TYPE_CURRENCY_DOUBLE = -6;
public static final int TYPE_CURRENCY_DOUBLE = -7;
/** Represents a triple currency symbol '¤¤¤'. */
public static final int TYPE_CURRENCY_TRIPLE = -7;
public static final int TYPE_CURRENCY_TRIPLE = -8;
/** Represents a quadruple currency symbol '¤¤¤¤'. */
public static final int TYPE_CURRENCY_QUAD = -8;
public static final int TYPE_CURRENCY_QUAD = -9;
/** Represents a quintuple currency symbol '¤¤¤¤¤'. */
public static final int TYPE_CURRENCY_QUINT = -9;
public static final int TYPE_CURRENCY_QUINT = -10;
/** Represents a sequence of six or more currency symbols. */
public static final int TYPE_CURRENCY_OVERFLOW = -15;
@ -267,6 +270,9 @@ public class AffixUtils {
return NumberFormat.Field.SIGN;
case TYPE_PLUS_SIGN:
return NumberFormat.Field.SIGN;
case TYPE_APPROXIMATELY_SIGN:
// TODO: Introduce a new field for the approximately sign?
return NumberFormat.Field.SIGN;
case TYPE_PERCENT:
return NumberFormat.Field.PERCENT;
case TYPE_PERMILLE:
@ -503,6 +509,8 @@ public class AffixUtils {
return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
case '+':
return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
case '~':
return makeTag(offset + count, TYPE_APPROXIMATELY_SIGN, STATE_BASE, 0);
case '%':
return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
case '‰':

View file

@ -142,6 +142,11 @@ public interface DecimalQuantity extends PluralRules.IFixedDecimal {
*/
public void adjustExponent(int delta);
/**
* Resets the DecimalQuantity to the value before adjustMagnitude and adjustExponent.
*/
public void resetExponent();
/**
* @return Whether the value represented by this {@link DecimalQuantity} is
* zero, infinity, or NaN.

View file

@ -233,6 +233,12 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
exponent = exponent + delta;
}
@Override
public void resetExponent() {
adjustMagnitude(exponent);
exponent = 0;
}
@Override
public boolean isHasIntegerValue() {
return scale >= 0;

View file

@ -29,6 +29,7 @@ public class MacroProps implements Cloneable {
public UnitWidth unitWidth;
public String unitDisplayCase;
public SignDisplay sign;
public Boolean approximately;
public DecimalSeparatorDisplay decimal;
public Scale scale;
public String usage;
@ -68,6 +69,8 @@ public class MacroProps implements Cloneable {
unitDisplayCase = fallback.unitDisplayCase;
if (sign == null)
sign = fallback.sign;
if (approximately == null)
approximately = fallback.approximately;
if (decimal == null)
decimal = fallback.decimal;
if (affixProvider == null)
@ -96,6 +99,7 @@ public class MacroProps implements Cloneable {
unitWidth,
unitDisplayCase,
sign,
approximately,
decimal,
affixProvider,
scale,
@ -125,6 +129,7 @@ public class MacroProps implements Cloneable {
&& Objects.equals(unitWidth, other.unitWidth)
&& Objects.equals(unitDisplayCase, other.unitDisplayCase)
&& Objects.equals(sign, other.sign)
&& Objects.equals(approximately, other.approximately)
&& Objects.equals(decimal, other.decimal)
&& Objects.equals(affixProvider, other.affixProvider)
&& Objects.equals(scale, other.scale)

View file

@ -42,6 +42,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
Field field;
SignDisplay signDisplay;
boolean perMilleReplacesPercent;
boolean approximately;
// Symbol details
DecimalFormatSymbols symbols;
@ -89,10 +90,13 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
* Whether to force a plus sign on positive numbers.
* @param perMille
* Whether to substitute the percent sign in the pattern with a permille sign.
* @param approximately
* Whether to prepend approximately to the sign
*/
public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
public void setPatternAttributes(SignDisplay signDisplay, boolean perMille, boolean approximately) {
this.signDisplay = signDisplay;
this.perMilleReplacesPercent = perMille;
this.approximately = approximately;
}
/**
@ -375,6 +379,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
PatternStringUtils.patternInfoToStringBuilder(patternInfo,
isPrefix,
PatternStringUtils.resolveSignDisplay(signDisplay, signum),
approximately,
plural,
perMilleReplacesPercent,
currentAffix);
@ -390,6 +395,8 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
return symbols.getMinusSignString();
case AffixUtils.TYPE_PLUS_SIGN:
return symbols.getPlusSignString();
case AffixUtils.TYPE_APPROXIMATELY_SIGN:
return symbols.getApproximatelySignString();
case AffixUtils.TYPE_PERCENT:
return symbols.getPercentString();
case AffixUtils.TYPE_PERMILLE:

View file

@ -295,6 +295,7 @@ public class PatternStringUtils {
String[][] table = new String[21][2];
int standIdx = toLocalized ? 0 : 1;
int localIdx = toLocalized ? 1 : 0;
// TODO: Add approximately sign here?
table[0][standIdx] = "%";
table[0][localIdx] = symbols.getPercentString();
table[1][standIdx] = "";
@ -430,6 +431,7 @@ public class PatternStringUtils {
AffixPatternProvider patternInfo,
boolean isPrefix,
PatternSignType patternSignType,
boolean approximately,
StandardPlural plural,
boolean perMilleReplacesPercent,
StringBuilder output) {
@ -441,7 +443,7 @@ public class PatternStringUtils {
// (If not, we will use the positive subpattern.)
boolean useNegativeAffixPattern = patternInfo.hasNegativeSubpattern()
&& (patternSignType == PatternSignType.NEG
|| (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
|| (patternInfo.negativeHasMinusSign() && (plusReplacesMinusSign || approximately)));
// Resolve the flags for the affix pattern.
int flags = 0;
@ -463,10 +465,25 @@ public class PatternStringUtils {
} else if (patternSignType == PatternSignType.NEG) {
prependSign = true;
} else {
prependSign = plusReplacesMinusSign;
prependSign = plusReplacesMinusSign || approximately;
}
// Compute the length of the affix pattern.
// What symbols should take the place of the sign placeholder?
String signSymbols = "-";
if (approximately) {
if (plusReplacesMinusSign) {
signSymbols = "~+";
} else if (patternSignType == PatternSignType.NEG) {
signSymbols = "~-";
} else {
signSymbols = "~";
}
} else if (plusReplacesMinusSign) {
signSymbols = "+";
}
// Compute the number of tokens in the affix pattern (signSymbols is considered one token).
int length = patternInfo.length(flags) + (prependSign ? 1 : 0);
// Finally, set the result into the StringBuilder.
@ -480,8 +497,13 @@ public class PatternStringUtils {
} else {
candidate = patternInfo.charAt(flags, index);
}
if (plusReplacesMinusSign && candidate == '-') {
candidate = '+';
if (candidate == '-') {
if (signSymbols.length() == 1) {
candidate = signSymbols.charAt(0);
} else {
output.append(signSymbols.charAt(0));
candidate = signSymbols.charAt(1);
}
}
if (perMilleReplacesPercent && candidate == '%') {
candidate = '‰';

View file

@ -108,9 +108,11 @@ public class AffixMatcher implements NumberParseMatcher {
}
// Generate Prefix
// TODO: Handle approximately sign?
PatternStringUtils.patternInfoToStringBuilder(patternInfo,
true,
type,
false,
StandardPlural.OTHER,
false,
sb);
@ -118,9 +120,11 @@ public class AffixMatcher implements NumberParseMatcher {
.fromAffixPattern(sb.toString(), factory, parseFlags);
// Generate Suffix
// TODO: Handle approximately sign?
PatternStringUtils.patternInfoToStringBuilder(patternInfo,
false,
type,
false,
StandardPlural.OTHER,
false,
sb);

View file

@ -362,7 +362,8 @@ class NumberFormatterImpl {
// The default middle modifier is weak (thus the false argument).
MutablePatternModifier patternMod = new MutablePatternModifier(false);
patternMod.setPatternInfo((macros.affixProvider != null) ? macros.affixProvider : patternInfo, null);
patternMod.setPatternAttributes(micros.sign, isPermille);
boolean approximately = (macros.approximately != null) ? macros.approximately : false;
patternMod.setPatternAttributes(micros.sign, isPermille, approximately);
if (patternMod.needsPlurals()) {
if (rules == null) {
// Lazily create PluralRules

View file

@ -13,6 +13,7 @@ import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.MacroProps;
import com.ibm.icu.impl.number.MicroProps;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.SimpleModifier;
@ -38,10 +39,10 @@ class NumberRangeFormatterImpl {
final NumberRangeFormatter.RangeCollapse fCollapse;
final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback;
// Should be final, but they are set in a helper function, not the constructor proper.
// TODO: Clean up to make these fields actually final.
// Should be final, but it is set in a helper function, not the constructor proper.
// TODO: Clean up to make this field actually final.
/* final */ String fRangePattern;
/* final */ SimpleModifier fApproximatelyModifier;
final NumberFormatterImpl fApproximatelyFormatter;
final StandardPluralRanges fPluralRanges;
@ -55,7 +56,8 @@ class NumberRangeFormatterImpl {
private static final class NumberRangeDataSink extends UResource.Sink {
String rangePattern;
String approximatelyPattern;
// Note: approximatelyPattern is unused since ICU 69.
// String approximatelyPattern;
// For use with SimpleFormatterImpl
StringBuilder sb;
@ -72,10 +74,13 @@ class NumberRangeFormatterImpl {
String pattern = value.getString();
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
}
/*
// Note: approximatelyPattern is unused since ICU 69.
if (key.contentEquals("approximately") && !hasApproxData()) {
String pattern = value.getString();
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 1, 1); // 1 arg, as in "~{0}"
}
*/
}
}
@ -83,21 +88,26 @@ class NumberRangeFormatterImpl {
return rangePattern != null;
}
/*
// Note: approximatelyPattern is unused since ICU 69.
private boolean hasApproxData() {
return approximatelyPattern != null;
}
*/
public boolean isComplete() {
return hasRangeData() && hasApproxData();
return hasRangeData() /* && hasApproxData() */;
}
public void fillInDefaults() {
if (!hasRangeData()) {
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}{1}", sb, 2, 2);
}
/*
if (!hasApproxData()) {
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1);
}
*/
}
}
@ -127,16 +137,20 @@ class NumberRangeFormatterImpl {
sink.fillInDefaults();
out.fRangePattern = sink.rangePattern;
out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false);
// out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false);
}
////////////////////
public NumberRangeFormatterImpl(RangeMacroProps macros) {
formatterImpl1 = new NumberFormatterImpl(macros.formatter1 != null ? macros.formatter1.resolve()
: NumberFormatter.withLocale(macros.loc).resolve());
formatterImpl2 = new NumberFormatterImpl(macros.formatter2 != null ? macros.formatter2.resolve()
: NumberFormatter.withLocale(macros.loc).resolve());
LocalizedNumberFormatter formatter1 = macros.formatter1 != null
? macros.formatter1.locale(macros.loc)
: NumberFormatter.withLocale(macros.loc);
LocalizedNumberFormatter formatter2 = macros.formatter2 != null
? macros.formatter2.locale(macros.loc)
: NumberFormatter.withLocale(macros.loc);
formatterImpl1 = new NumberFormatterImpl(formatter1.resolve());
formatterImpl2 = new NumberFormatterImpl(formatter2.resolve());
fSameFormatters = macros.sameFormatters != 0;
fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO;
fIdentityFallback = macros.identityFallback != null ? macros.identityFallback
@ -148,6 +162,17 @@ class NumberRangeFormatterImpl {
}
getNumberRangeData(macros.loc, nsName, this);
if (fSameFormatters && (
fIdentityFallback == RangeIdentityFallback.APPROXIMATELY ||
fIdentityFallback == RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)) {
MacroProps approximatelyMacros = new MacroProps();
approximatelyMacros.approximately = true;
fApproximatelyFormatter = new NumberFormatterImpl(
formatter1.macros(approximatelyMacros).resolve());
} else {
fApproximatelyFormatter = null;
}
// TODO: Get locale from PluralRules instead?
fPluralRanges = StandardPluralRanges.forLocale(macros.loc);
}
@ -230,12 +255,14 @@ class NumberRangeFormatterImpl {
private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string,
MicroProps micros1, MicroProps micros2) {
if (fSameFormatters) {
int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
// Re-format using the approximately formatter:
quantity1.resetExponent();
MicroProps microsAppx = fApproximatelyFormatter.preProcess(quantity1);
int length = NumberFormatterImpl.writeNumber(microsAppx, quantity1, string, 0);
// HEURISTIC: Desired modifier order: inner, middle, approximately, outer.
length += micros1.modInner.apply(string, 0, length);
length += micros1.modMiddle.apply(string, 0, length);
length += fApproximatelyModifier.apply(string, 0, length);
micros1.modOuter.apply(string, 0, length);
length += microsAppx.modInner.apply(string, 0, length);
length += microsAppx.modMiddle.apply(string, 0, length);
microsAppx.modOuter.apply(string, 0, length);
} else {
formatRange(quantity1, quantity2, string, micros1, micros2);
}

View file

@ -820,6 +820,23 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
}
}
/**
* @internal Technical Preview
*/
public String getApproximatelySignString() {
return approximatelyString;
}
/**
* @internal Technical Preview
*/
public void setApproximatelySignString(String approximatelySignString) {
if (approximatelySignString == null) {
throw new NullPointerException("The input plus sign is null");
}
this.approximatelyString = approximatelySignString;
}
/**
* Returns the string denoting the local currency.
* @return the local currency String.
@ -1269,6 +1286,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
padEscape == other.padEscape &&
plusSign == other.plusSign &&
plusString.equals(other.plusString) &&
approximatelyString.equals(other.approximatelyString) &&
exponentSeparator.equals(other.exponentSeparator) &&
monetarySeparator == other.monetarySeparator &&
monetaryGroupingSeparator == other.monetaryGroupingSeparator &&
@ -1304,7 +1322,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
"nan",
"currencyDecimal",
"currencyGroup",
"superscriptingExponent"
"superscriptingExponent",
"approximatelySign",
};
/*
@ -1341,7 +1360,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
"NaN", // NaN
null, // currency decimal
null, // currency group
"\u00D7" // superscripting exponent
"\u00D7", // superscripting exponent
"~", // // approximately sign
};
/**
@ -1414,6 +1434,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
setMonetaryDecimalSeparatorString(numberElements[9]);
setMonetaryGroupingSeparatorString(numberElements[10]);
setExponentMultiplicationSign(numberElements[11]);
setApproximatelySignString(numberElements[12]);
digit = '#'; // Localized pattern character no longer in CLDR
padEscape = '*';
@ -1616,6 +1637,10 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
monetaryGroupingSeparatorString = String.valueOf(monetaryGroupingSeparator);
}
}
if (serialVersionOnStream < 10) {
// Approximately sign
approximatelyString = "~"; // fallback
}
serialVersionOnStream = currentSerialVersion;
@ -1784,6 +1809,13 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
*/
private String plusString;
/**
* The string used to indicate an approximately sign.
* @serial
* @since ICU 69
*/
private String approximatelyString;
/**
* String denoting the local currency, e.g. "$".
* @serial
@ -1890,7 +1922,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
// - 7 for ICU 52, which includes the minusString and plusString fields
// - 8 for ICU 54, which includes exponentMultiplicationSign field.
// - 9 for ICU 58, which includes a series of String symbol fields.
private static final int currentSerialVersion = 8;
// - 10 for ICU 69, which includes the approximatelyString field.
private static final int currentSerialVersion = 10;
/**
* Describes the version of <code>DecimalFormatSymbols</code> present on the stream.

View file

@ -951,6 +951,12 @@ public class DecimalQuantity_SimpleStorage implements DecimalQuantity {
origPrimaryScale = origPrimaryScale + delta;
}
@Override
public void resetExponent() {
adjustMagnitude(origPrimaryScale);
origPrimaryScale = 0;
}
@Override
public boolean isHasIntegerValue() {
return scaleBigDecimal(toBigDecimal()) >= 0;

View file

@ -24,6 +24,8 @@ public class AffixUtilsTest {
return "";
case AffixUtils.TYPE_PLUS_SIGN:
return "\u061C+";
case AffixUtils.TYPE_APPROXIMATELY_SIGN:
return "";
case AffixUtils.TYPE_PERCENT:
return "٪\u061C";
case AffixUtils.TYPE_PERMILLE:
@ -81,6 +83,7 @@ public class AffixUtilsTest {
{ "-!", false, 2, "!" },
{ "+", false, 1, "\u061C+" },
{ "+!", false, 2, "\u061C+!" },
{ "~", false, 1, "" },
{ "", false, 1, "؉" },
{ "‰!", false, 2, "؉!" },
{ "-x", false, 2, "x" },
@ -188,7 +191,7 @@ public class AffixUtilsTest {
{ "", "" },
{ "-", "1" },
{ "'-'", "-" },
{ "- + % ‰ ¤ ¤¤ ¤¤¤ ¤¤¤¤ ¤¤¤¤¤", "1 2 3 4 5 6 7 8 9" },
{ "- + ~ % ‰ ¤ ¤¤ ¤¤¤ ¤¤¤¤ ¤¤¤¤¤", "1 2 3 4 5 6 7 8 9 10" },
{ "'¤¤¤¤¤¤'", "¤¤¤¤¤¤" },
{ "¤¤¤¤¤¤", "\uFFFD" } };
@ -211,7 +214,7 @@ public class AffixUtilsTest {
// Test insertion position
sb.clear();
sb.append("abcdefg", null);
AffixUtils.unescape("-+%", sb, 4, provider, null);
AffixUtils.unescape("-+~", sb, 4, provider, null);
assertEquals("Symbol provider into middle", "abcd123efg", sb.toString());
}

View file

@ -27,7 +27,7 @@ public class MutablePatternModifierTest {
public void basic() {
MutablePatternModifier mod = new MutablePatternModifier(false);
mod.setPatternInfo(PatternStringParser.parseToPatternInfo("a0b"), null);
mod.setPatternAttributes(SignDisplay.AUTO, false);
mod.setPatternAttributes(SignDisplay.AUTO, false, false);
mod.setSymbols(DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
Currency.getInstance("USD"),
UnitWidth.SHORT,
@ -36,7 +36,7 @@ public class MutablePatternModifierTest {
mod.setNumberProperties(Signum.POS, null);
assertEquals("a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.ALWAYS, false);
mod.setPatternAttributes(SignDisplay.ALWAYS, false, false);
assertEquals("+a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setNumberProperties(Signum.POS_ZERO, null);
@ -45,22 +45,27 @@ public class MutablePatternModifierTest {
mod.setNumberProperties(Signum.NEG_ZERO, null);
assertEquals("-a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.EXCEPT_ZERO, false);
mod.setPatternAttributes(SignDisplay.EXCEPT_ZERO, false, false);
assertEquals("a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setNumberProperties(Signum.NEG, null);
assertEquals("-a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.NEVER, false);
mod.setPatternAttributes(SignDisplay.NEVER, false, false);
assertEquals("a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.AUTO, false, true);
mod.setNumberProperties(Signum.POS, null);
assertEquals("Pattern a0b", "~a", getPrefix(mod));
assertEquals("Pattern a0b", "b", getSuffix(mod));
mod.setPatternInfo(PatternStringParser.parseToPatternInfo("a0b;c-0d"), null);
mod.setPatternAttributes(SignDisplay.AUTO, false);
mod.setPatternAttributes(SignDisplay.AUTO, false, false);
mod.setNumberProperties(Signum.POS, null);
assertEquals("a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.ALWAYS, false);
mod.setPatternAttributes(SignDisplay.ALWAYS, false, false);
assertEquals("c+", getPrefix(mod));
assertEquals("d", getSuffix(mod));
mod.setNumberProperties(Signum.POS_ZERO, null);
@ -69,13 +74,13 @@ public class MutablePatternModifierTest {
mod.setNumberProperties(Signum.NEG_ZERO, null);
assertEquals("c-", getPrefix(mod));
assertEquals("d", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.EXCEPT_ZERO, false);
mod.setPatternAttributes(SignDisplay.EXCEPT_ZERO, false, false);
assertEquals("a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
mod.setNumberProperties(Signum.NEG, null);
assertEquals("c-", getPrefix(mod));
assertEquals("d", getSuffix(mod));
mod.setPatternAttributes(SignDisplay.NEVER, false);
mod.setPatternAttributes(SignDisplay.NEVER, false, false);
assertEquals("a", getPrefix(mod));
assertEquals("b", getSuffix(mod));
}
@ -84,7 +89,7 @@ public class MutablePatternModifierTest {
public void mutableEqualsImmutable() {
MutablePatternModifier mod = new MutablePatternModifier(false);
mod.setPatternInfo(PatternStringParser.parseToPatternInfo("a0b;c-0d"), null);
mod.setPatternAttributes(SignDisplay.AUTO, false);
mod.setPatternAttributes(SignDisplay.AUTO, false, false);
mod.setSymbols(DecimalFormatSymbols.getInstance(ULocale.ENGLISH), null, UnitWidth.SHORT, null);
DecimalQuantity fq = new DecimalQuantity_DualStorageBCD(1);
@ -102,7 +107,7 @@ public class MutablePatternModifierTest {
FormattedStringBuilder nsb3 = new FormattedStringBuilder();
MicroProps micros3 = new MicroProps(false);
mod.addToChain(micros3);
mod.setPatternAttributes(SignDisplay.ALWAYS, false);
mod.setPatternAttributes(SignDisplay.ALWAYS, false, false);
mod.processQuantity(fq);
micros3.modMiddle.apply(nsb3, 0, 0);
@ -114,7 +119,7 @@ public class MutablePatternModifierTest {
public void patternWithNoPlaceholder() {
MutablePatternModifier mod = new MutablePatternModifier(false);
mod.setPatternInfo(PatternStringParser.parseToPatternInfo("abc"), null);
mod.setPatternAttributes(SignDisplay.AUTO, false);
mod.setPatternAttributes(SignDisplay.AUTO, false, false);
mod.setSymbols(DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
Currency.getInstance("USD"),
UnitWidth.SHORT,

View file

@ -2108,6 +2108,26 @@ public class NumberFormatterApiTest extends TestFmwk {
ULocale.forLanguageTag("lu"),
123.12,
"123,12 CN¥");
// de-CH has currency pattern "¤ #,##0.00;¤-#,##0.00"
assertFormatSingle(
"Sign position on negative number with pattern spacing",
"currency/RON",
"currency/RON",
NumberFormatter.with().unit(RON),
ULocale.forLanguageTag("de-CH"),
-123.12,
"RON-123.12");
// TODO(CLDR-13044): Move the sign to the inside of the number
assertFormatSingle(
"Sign position on negative number with currency spacing",
"currency/RON",
"currency/RON",
NumberFormatter.with().unit(RON),
ULocale.forLanguageTag("en"),
-123.12,
"-RON 123.12");
}
public static class UnitInflectionTestCase {

View file

@ -42,6 +42,7 @@ import com.ibm.icu.util.UResourceBundle;
public class NumberRangeFormatterTest extends TestFmwk {
private static final Currency USD = Currency.getInstance("USD");
private static final Currency CHF = Currency.getInstance("CHF");
private static final Currency GBP = Currency.getInstance("GBP");
private static final Currency PTE = Currency.getInstance("PTE");
@ -131,14 +132,14 @@ public class NumberRangeFormatterTest extends TestFmwk {
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME)),
new ULocale("fr-FR"),
"15\u00A0degrés Fahrenheit",
"5\u00A0degrés Fahrenheit",
"5\u00A0degrés Fahrenheit",
"5\u00A0degrés Fahrenheit",
"5\u00A0degrés Fahrenheit",
"03\u00A0degrés Fahrenheit",
"0\u00A0degré Fahrenheit",
"0\u00A0degré Fahrenheit",
"33\u202F000\u00A0degrés Fahrenheit",
"3\u202F0005\u202F000\u00A0degrés Fahrenheit",
"4\u202F9995\u202F001\u00A0degrés Fahrenheit",
"5\u202F000\u00A0degrés Fahrenheit",
"5\u202F000\u00A0degrés Fahrenheit",
"5\u202F0005\u202F000\u202F000\u00A0degrés Fahrenheit");
assertFormatRange(
@ -146,14 +147,14 @@ public class NumberRangeFormatterTest extends TestFmwk {
NumberRangeFormatter.with(),
new ULocale("ja"),
"15",
" 5",
" 5",
"5",
"5",
"03",
" 0",
"0",
"33,000",
"3,0005,000",
"4,9995,001",
" 5,000",
"5,000",
"5,0005,000,000");
assertFormatRange(
@ -853,6 +854,74 @@ public class NumberRangeFormatterTest extends TestFmwk {
}
}
@Test
public void test21358_SignPosition() {
// de-CH has currency pattern "¤ #,##0.00;¤-#,##0.00"
assertFormatRange(
"Approximately sign position with spacing from pattern",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(CHF)),
ULocale.forLanguageTag("de-CH"),
"CHF 1.005.00",
"CHF≈5.00",
"CHF≈5.00",
"CHF 0.003.00",
"CHF≈0.00",
"CHF 3.003000.00",
"CHF 3000.005000.00",
"CHF 4999.005001.00",
"CHF≈5000.00",
"CHF 5000.005000000.00");
// TODO(CLDR-13044): Move the sign to the inside of the number
assertFormatRange(
"Approximately sign position with currency spacing",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(CHF)),
ULocale.forLanguageTag("en-US"),
"CHF 1.005.00",
"~CHF 5.00",
"~CHF 5.00",
"CHF 0.003.00",
"~CHF 0.00",
"CHF 3.003,000.00",
"CHF 3,000.005,000.00",
"CHF 4,999.005,001.00",
"~CHF 5,000.00",
"CHF 5,000.005,000,000.00");
{
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter
.withLocale(ULocale.forLanguageTag("de-CH"));
String actual = lnrf.formatRange(-2, 3).toString();
assertEquals("Negative to positive range", "-2 3", actual);
}
{
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter
.withLocale(ULocale.forLanguageTag("de-CH"))
.numberFormatterBoth(NumberFormatter.forSkeleton("%"));
String actual = lnrf.formatRange(-2, 3).toString();
assertEquals("Negative to positive percent", "-2% 3%", actual);
}
{
// TODO(CLDR-14111): Add spacing between range separator and sign
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter
.withLocale(ULocale.forLanguageTag("de-CH"));
String actual = lnrf.formatRange(2, -3).toString();
assertEquals("Positive to negative range", "2-3", actual);
}
{
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter
.withLocale(ULocale.forLanguageTag("de-CH"))
.numberFormatterBoth(NumberFormatter.forSkeleton("%"));
String actual = lnrf.formatRange(2, -3).toString();
assertEquals("Positive to negative percent", "2% -3%", actual);
}
}
static void assertFormatRange(
String message,
UnlocalizedNumberRangeFormatter f,