]> git.saurik.com Git - apple/icu.git/blob - icuSources/i18n/number_mapper.cpp
a13bcb00947ba16c2f2f2a4b1e256a4d5a2deaeb
[apple/icu.git] / icuSources / i18n / number_mapper.cpp
1 // © 2018 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
3
4 #include "unicode/utypes.h"
5
6 #if !UCONFIG_NO_FORMATTING
7
8 // Allow implicit conversion from char16_t* to UnicodeString for this file:
9 // Helpful in toString methods and elsewhere.
10 #define UNISTR_FROM_STRING_EXPLICIT
11
12 #include "number_mapper.h"
13 #include "number_patternstring.h"
14 #include "unicode/errorcode.h"
15 #include "number_utils.h"
16 #include "number_currencysymbols.h"
17
18 using namespace icu;
19 using namespace icu::number;
20 using namespace icu::number::impl;
21
22
23 UnlocalizedNumberFormatter NumberPropertyMapper::create(const DecimalFormatProperties& properties,
24 const DecimalFormatSymbols& symbols,
25 DecimalFormatWarehouse& warehouse,
26 UErrorCode& status) {
27 return NumberFormatter::with().macros(oldToNew(properties, symbols, warehouse, nullptr, status));
28 }
29
30 UnlocalizedNumberFormatter NumberPropertyMapper::create(const DecimalFormatProperties& properties,
31 const DecimalFormatSymbols& symbols,
32 DecimalFormatWarehouse& warehouse,
33 DecimalFormatProperties& exportedProperties,
34 UErrorCode& status) {
35 return NumberFormatter::with().macros(
36 oldToNew(
37 properties, symbols, warehouse, &exportedProperties, status));
38 }
39
40 MacroProps NumberPropertyMapper::oldToNew(const DecimalFormatProperties& properties,
41 const DecimalFormatSymbols& symbols,
42 DecimalFormatWarehouse& warehouse,
43 DecimalFormatProperties* exportedProperties,
44 UErrorCode& status) {
45 MacroProps macros;
46 Locale locale = symbols.getLocale();
47
48 /////////////
49 // SYMBOLS //
50 /////////////
51
52 macros.symbols.setTo(symbols);
53
54 //////////////////
55 // PLURAL RULES //
56 //////////////////
57
58 if (!properties.currencyPluralInfo.fPtr.isNull()) {
59 macros.rules = properties.currencyPluralInfo.fPtr->getPluralRules();
60 }
61
62 /////////////
63 // AFFIXES //
64 /////////////
65
66 AffixPatternProvider* affixProvider;
67 if (properties.currencyPluralInfo.fPtr.isNull()) {
68 warehouse.currencyPluralInfoAPP.setToBogus();
69 warehouse.propertiesAPP.setTo(properties, status);
70 affixProvider = &warehouse.propertiesAPP;
71 } else {
72 warehouse.currencyPluralInfoAPP.setTo(*properties.currencyPluralInfo.fPtr, properties, status);
73 warehouse.propertiesAPP.setToBogus();
74 affixProvider = &warehouse.currencyPluralInfoAPP;
75 }
76 macros.affixProvider = affixProvider;
77
78 ///////////
79 // UNITS //
80 ///////////
81
82 bool useCurrency = (
83 !properties.currency.isNull() ||
84 !properties.currencyPluralInfo.fPtr.isNull() ||
85 !properties.currencyUsage.isNull() ||
86 affixProvider->hasCurrencySign());
87 CurrencyUnit currency = resolveCurrency(properties, locale, status);
88 UCurrencyUsage currencyUsage = properties.currencyUsage.getOrDefault(UCURR_USAGE_STANDARD);
89 if (useCurrency) {
90 // NOTE: Slicing is OK.
91 macros.unit = currency; // NOLINT
92 }
93 warehouse.currencySymbols = {currency, locale, symbols, status};
94 macros.currencySymbols = &warehouse.currencySymbols;
95
96 ///////////////////////
97 // ROUNDING STRATEGY //
98 ///////////////////////
99
100 int32_t maxInt = properties.maximumIntegerDigits;
101 int32_t minInt = properties.minimumIntegerDigits;
102 int32_t maxFrac = properties.maximumFractionDigits;
103 int32_t minFrac = properties.minimumFractionDigits;
104 int32_t minSig = properties.minimumSignificantDigits;
105 int32_t maxSig = properties.maximumSignificantDigits;
106 double roundingIncrement = properties.roundingIncrement;
107 bool didAdjustRoundIncr = false;
108 RoundingMode roundingMode = properties.roundingMode.getOrDefault(UNUM_ROUND_HALFEVEN);
109 bool explicitMinMaxFrac = minFrac != -1 || maxFrac != -1;
110 bool explicitMinMaxSig = minSig != -1 || maxSig != -1;
111 // Resolve min/max frac for currencies, required for the validation logic and for when minFrac or
112 // maxFrac was
113 // set (but not both) on a currency instance.
114 // NOTE: Increments are handled in "Precision.constructCurrency()".
115 if (useCurrency && (minFrac == -1 || maxFrac == -1)) {
116 int32_t digits = ucurr_getDefaultFractionDigitsForUsage(
117 currency.getISOCurrency(), currencyUsage, &status);
118 if (minFrac == -1 && maxFrac == -1) {
119 minFrac = digits;
120 maxFrac = digits;
121 } else if (minFrac == -1) {
122 minFrac = std::min(maxFrac, digits);
123 } else /* if (maxFrac == -1) */ {
124 maxFrac = std::max(minFrac, digits);
125 }
126 }
127 // Validate min/max int/frac.
128 // For backwards compatibility, minimum overrides maximum if the two conflict.
129 // The following logic ensures that there is always a minimum of at least one digit.
130 if (minInt == 0 && maxFrac != 0) {
131 // Force a digit after the decimal point.
132 minFrac = minFrac <= 0 ? 1 : minFrac;
133 maxFrac = maxFrac < 0 ? -1 : maxFrac < minFrac ? minFrac : maxFrac;
134 minInt = 0;
135 maxInt = maxInt < 0 ? -1 : maxInt > kMaxIntFracSig ? -1 : maxInt;
136 } else {
137 // Force a digit before the decimal point.
138 minFrac = minFrac < 0 ? 0 : minFrac;
139 maxFrac = maxFrac < 0 ? -1 : maxFrac < minFrac ? minFrac : maxFrac;
140 minInt = minInt <= 0 ? 1 : minInt > kMaxIntFracSig ? 1 : minInt;
141 maxInt = maxInt < 0 ? -1 : maxInt < minInt ? minInt : maxInt > kMaxIntFracSig ? -1 : maxInt;
142 }
143 Precision precision;
144 if (!properties.currencyUsage.isNull()) {
145 precision = Precision::constructCurrency(currencyUsage).withCurrency(currency);
146 } else if (roundingIncrement != 0.0) {
147 double roundingIncrAdj = roundingIncrement;
148 if (!explicitMinMaxSig && PatternStringUtils::ignoreRoundingIncrement(&roundingIncrAdj, maxFrac)) {
149 precision = Precision::constructFraction(minFrac, maxFrac);
150 } else {
151 double delta = (roundingIncrement/roundingIncrAdj) - 1.0;
152 if (delta > 0.001 || delta < -0.001) {
153 roundingIncrAdj = roundingIncrement;
154 } else {
155 didAdjustRoundIncr = true;
156 }
157 if (explicitMinMaxSig) {
158 minSig = minSig < 1 ? 1 : minSig > kMaxIntFracSig ? kMaxIntFracSig : minSig;
159 maxSig = maxSig < 0 ? kMaxIntFracSig : maxSig < minSig ? minSig : maxSig > kMaxIntFracSig
160 ? kMaxIntFracSig : maxSig;
161 precision = Precision::constructIncrementSignificant(roundingIncrAdj, minSig, maxSig); // Apple rdar://52538227
162 } else {
163 precision = Precision::constructIncrement(roundingIncrAdj, minFrac);
164 }
165 }
166 } else if (explicitMinMaxSig) {
167 minSig = minSig < 1 ? 1 : minSig > kMaxIntFracSig ? kMaxIntFracSig : minSig;
168 maxSig = maxSig < 0 ? kMaxIntFracSig : maxSig < minSig ? minSig : maxSig > kMaxIntFracSig
169 ? kMaxIntFracSig : maxSig;
170 precision = Precision::constructSignificant(minSig, maxSig);
171 } else if (explicitMinMaxFrac) {
172 precision = Precision::constructFraction(minFrac, maxFrac);
173 } else if (useCurrency) {
174 precision = Precision::constructCurrency(currencyUsage);
175 }
176 if (!precision.isBogus()) {
177 precision.fRoundingMode = roundingMode;
178 macros.precision = precision;
179 }
180
181 // Apple addition for <rdar://problem/39240173>
182 macros.adjustDoublePrecision = (!properties.formatFullPrecision && !explicitMinMaxSig && maxFrac>15);
183
184 ///////////////////
185 // INTEGER WIDTH //
186 ///////////////////
187
188 macros.integerWidth = IntegerWidth(
189 static_cast<digits_t>(minInt),
190 static_cast<digits_t>(maxInt),
191 properties.formatFailIfMoreThanMaxDigits);
192
193 ///////////////////////
194 // GROUPING STRATEGY //
195 ///////////////////////
196
197 macros.grouper = Grouper::forProperties(properties);
198
199 /////////////
200 // PADDING //
201 /////////////
202
203 if (properties.formatWidth > 0) {
204 macros.padder = Padder::forProperties(properties);
205 }
206
207 ///////////////////////////////
208 // DECIMAL MARK ALWAYS SHOWN //
209 ///////////////////////////////
210
211 macros.decimal = properties.decimalSeparatorAlwaysShown ? UNUM_DECIMAL_SEPARATOR_ALWAYS
212 : UNUM_DECIMAL_SEPARATOR_AUTO;
213
214 ///////////////////////
215 // SIGN ALWAYS SHOWN //
216 ///////////////////////
217
218 macros.sign = properties.signAlwaysShown ? UNUM_SIGN_ALWAYS : UNUM_SIGN_AUTO;
219
220 /////////////////////////
221 // SCIENTIFIC NOTATION //
222 /////////////////////////
223
224 if (properties.minimumExponentDigits != -1) {
225 // Scientific notation is required.
226 // This whole section feels like a hack, but it is needed for regression tests.
227 // The mapping from property bag to scientific notation is nontrivial due to LDML rules.
228 if (maxInt > 8) {
229 // But #13110: The maximum of 8 digits has unknown origins and is not in the spec.
230 // If maxInt is greater than 8, it is set to minInt, even if minInt is greater than 8.
231 maxInt = minInt;
232 macros.integerWidth = IntegerWidth::zeroFillTo(minInt).truncateAt(maxInt);
233 } else if (maxInt > minInt && minInt > 1) {
234 // Bug #13289: if maxInt > minInt > 1, then minInt should be 1.
235 minInt = 1;
236 macros.integerWidth = IntegerWidth::zeroFillTo(minInt).truncateAt(maxInt);
237 }
238 int engineering = maxInt < 0 ? -1 : maxInt;
239 macros.notation = ScientificNotation(
240 // Engineering interval:
241 static_cast<int8_t>(engineering),
242 // Enforce minimum integer digits (for patterns like "000.00E0"):
243 (engineering == minInt),
244 // Minimum exponent digits:
245 static_cast<digits_t>(properties.minimumExponentDigits),
246 // Exponent sign always shown:
247 properties.exponentSignAlwaysShown ? UNUM_SIGN_ALWAYS : UNUM_SIGN_AUTO);
248 // Scientific notation also involves overriding the rounding mode.
249 // TODO: Overriding here is a bit of a hack. Should this logic go earlier?
250 if (macros.precision.fType == Precision::PrecisionType::RND_FRACTION) {
251 // For the purposes of rounding, get the original min/max int/frac, since the local
252 // variables have been manipulated for display purposes.
253 int maxInt_ = properties.maximumIntegerDigits;
254 int minInt_ = properties.minimumIntegerDigits;
255 int minFrac_ = properties.minimumFractionDigits;
256 int maxFrac_ = properties.maximumFractionDigits;
257 if (minInt_ == 0 && maxFrac_ == 0) {
258 // Patterns like "#E0" and "##E0", which mean no rounding!
259 macros.precision = Precision::unlimited();
260 } else if (minInt_ == 0 && minFrac_ == 0) {
261 // Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1
262 macros.precision = Precision::constructSignificant(1, maxFrac_ + 1);
263 } else {
264 int maxSig_ = minInt_ + maxFrac_;
265 // Bug #20058: if maxInt_ > minInt_ > 1, then minInt_ should be 1.
266 if (maxInt_ > minInt_ && minInt_ > 1) {
267 minInt_ = 1;
268 }
269 int minSig_ = minInt_ + minFrac_;
270 // To avoid regression, maxSig is not reset when minInt_ set to 1.
271 // TODO: Reset maxSig_ = 1 + minFrac_ to follow the spec.
272 macros.precision = Precision::constructSignificant(minSig_, maxSig_);
273 }
274 macros.precision.fRoundingMode = roundingMode;
275 }
276 }
277
278 //////////////////////
279 // COMPACT NOTATION //
280 //////////////////////
281
282 if (!properties.compactStyle.isNull()) {
283 if (properties.compactStyle.getNoError() == UNumberCompactStyle::UNUM_LONG) {
284 macros.notation = Notation::compactLong();
285 } else {
286 macros.notation = Notation::compactShort();
287 }
288 // Do not forward the affix provider.
289 macros.affixProvider = nullptr;
290 }
291
292 /////////////////
293 // MULTIPLIERS //
294 /////////////////
295
296 macros.scale = scaleFromProperties(properties);
297
298 //////////////////////
299 // PROPERTY EXPORTS //
300 //////////////////////
301
302 if (exportedProperties != nullptr) {
303
304 exportedProperties->currency = currency;
305 exportedProperties->roundingMode = roundingMode;
306 exportedProperties->minimumIntegerDigits = minInt;
307 exportedProperties->maximumIntegerDigits = maxInt == -1 ? INT32_MAX : maxInt;
308
309 Precision rounding_;
310 if (precision.fType == Precision::PrecisionType::RND_CURRENCY) {
311 rounding_ = precision.withCurrency(currency, status);
312 } else {
313 rounding_ = precision;
314 }
315 int minFrac_ = minFrac;
316 int maxFrac_ = maxFrac;
317 int minSig_ = minSig;
318 int maxSig_ = maxSig;
319 double increment_ = 0.0;
320 if (rounding_.fType == Precision::PrecisionType::RND_FRACTION) {
321 minFrac_ = rounding_.fUnion.fracSig.fMinFrac;
322 maxFrac_ = rounding_.fUnion.fracSig.fMaxFrac;
323 } else if (rounding_.fType == Precision::PrecisionType::RND_INCREMENT
324 || rounding_.fType == Precision::PrecisionType::RND_INCREMENT_ONE
325 || rounding_.fType == Precision::PrecisionType::RND_INCREMENT_FIVE) {
326 increment_ = (didAdjustRoundIncr)? roundingIncrement: rounding_.fUnion.increment.fIncrement; // rdar://51452216
327 minFrac_ = rounding_.fUnion.increment.fMinFrac;
328 maxFrac_ = rounding_.fUnion.increment.fMinFrac;
329 } else if (rounding_.fType == Precision::PrecisionType::RND_SIGNIFICANT) {
330 minSig_ = rounding_.fUnion.fracSig.fMinSig;
331 maxSig_ = rounding_.fUnion.fracSig.fMaxSig;
332 } else if (rounding_.fType == Precision::PrecisionType::RND_INCREMENT_SIGNIFICANT) { // Apple rdar://52538227
333 increment_ = (didAdjustRoundIncr)? roundingIncrement: rounding_.fUnion.incrSig.fIncrement; // rdar://51452216
334 minSig_ = rounding_.fUnion.incrSig.fMinSig;
335 maxSig_ = rounding_.fUnion.incrSig.fMaxSig;
336 }
337
338 exportedProperties->minimumFractionDigits = minFrac_;
339 exportedProperties->maximumFractionDigits = maxFrac_;
340 exportedProperties->minimumSignificantDigits = minSig_;
341 exportedProperties->maximumSignificantDigits = maxSig_;
342 exportedProperties->roundingIncrement = increment_;
343 }
344
345 return macros;
346 }
347
348
349 void PropertiesAffixPatternProvider::setTo(const DecimalFormatProperties& properties, UErrorCode& status) {
350 fBogus = false;
351
352 // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the
353 // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows:
354 //
355 // 1) If the explicit setting is present for the field, use it.
356 // 2) Otherwise, follows UTS 35 rules based on the pattern string.
357 //
358 // Importantly, the explicit setters affect only the one field they override. If you set the positive
359 // prefix, that should not affect the negative prefix.
360
361 // Convenience: Extract the properties into local variables.
362 // Variables are named with three chars: [p/n][p/s][o/p]
363 // [p/n] => p for positive, n for negative
364 // [p/s] => p for prefix, s for suffix
365 // [o/p] => o for escaped custom override string, p for pattern string
366 UnicodeString ppo = AffixUtils::escape(properties.positivePrefix);
367 UnicodeString pso = AffixUtils::escape(properties.positiveSuffix);
368 UnicodeString npo = AffixUtils::escape(properties.negativePrefix);
369 UnicodeString nso = AffixUtils::escape(properties.negativeSuffix);
370 const UnicodeString& ppp = properties.positivePrefixPattern;
371 const UnicodeString& psp = properties.positiveSuffixPattern;
372 const UnicodeString& npp = properties.negativePrefixPattern;
373 const UnicodeString& nsp = properties.negativeSuffixPattern;
374
375 if (!properties.positivePrefix.isBogus()) {
376 posPrefix = ppo;
377 } else if (!ppp.isBogus()) {
378 posPrefix = ppp;
379 } else {
380 // UTS 35: Default positive prefix is empty string.
381 posPrefix = u"";
382 }
383
384 if (!properties.positiveSuffix.isBogus()) {
385 posSuffix = pso;
386 } else if (!psp.isBogus()) {
387 posSuffix = psp;
388 } else {
389 // UTS 35: Default positive suffix is empty string.
390 posSuffix = u"";
391 }
392
393 if (!properties.negativePrefix.isBogus()) {
394 negPrefix = npo;
395 } else if (!npp.isBogus()) {
396 negPrefix = npp;
397 } else {
398 // UTS 35: Default negative prefix is "-" with positive prefix.
399 // Important: We prepend the "-" to the pattern, not the override!
400 negPrefix = ppp.isBogus() ? u"-" : u"-" + ppp;
401 }
402
403 if (!properties.negativeSuffix.isBogus()) {
404 negSuffix = nso;
405 } else if (!nsp.isBogus()) {
406 negSuffix = nsp;
407 } else {
408 // UTS 35: Default negative prefix is the positive prefix.
409 negSuffix = psp.isBogus() ? u"" : psp;
410 }
411
412 // For declaring if this is a currency pattern, we need to look at the
413 // original pattern, not at any user-specified overrides.
414 isCurrencyPattern = (
415 AffixUtils::hasCurrencySymbols(ppp, status) ||
416 AffixUtils::hasCurrencySymbols(psp, status) ||
417 AffixUtils::hasCurrencySymbols(npp, status) ||
418 AffixUtils::hasCurrencySymbols(nsp, status));
419 }
420
421 char16_t PropertiesAffixPatternProvider::charAt(int flags, int i) const {
422 return getStringInternal(flags).charAt(i);
423 }
424
425 int PropertiesAffixPatternProvider::length(int flags) const {
426 return getStringInternal(flags).length();
427 }
428
429 UnicodeString PropertiesAffixPatternProvider::getString(int32_t flags) const {
430 return getStringInternal(flags);
431 }
432
433 const UnicodeString& PropertiesAffixPatternProvider::getStringInternal(int32_t flags) const {
434 bool prefix = (flags & AFFIX_PREFIX) != 0;
435 bool negative = (flags & AFFIX_NEGATIVE_SUBPATTERN) != 0;
436 if (prefix && negative) {
437 return negPrefix;
438 } else if (prefix) {
439 return posPrefix;
440 } else if (negative) {
441 return negSuffix;
442 } else {
443 return posSuffix;
444 }
445 }
446
447 bool PropertiesAffixPatternProvider::positiveHasPlusSign() const {
448 // TODO: Change the internal APIs to propagate out the error?
449 ErrorCode localStatus;
450 return AffixUtils::containsType(posPrefix, TYPE_PLUS_SIGN, localStatus) ||
451 AffixUtils::containsType(posSuffix, TYPE_PLUS_SIGN, localStatus);
452 }
453
454 bool PropertiesAffixPatternProvider::hasNegativeSubpattern() const {
455 return (
456 (negSuffix != posSuffix) ||
457 negPrefix.tempSubString(1) != posPrefix ||
458 negPrefix.charAt(0) != u'-'
459 );
460 }
461
462 bool PropertiesAffixPatternProvider::negativeHasMinusSign() const {
463 ErrorCode localStatus;
464 return AffixUtils::containsType(negPrefix, TYPE_MINUS_SIGN, localStatus) ||
465 AffixUtils::containsType(negSuffix, TYPE_MINUS_SIGN, localStatus);
466 }
467
468 bool PropertiesAffixPatternProvider::hasCurrencySign() const {
469 return isCurrencyPattern;
470 }
471
472 bool PropertiesAffixPatternProvider::containsSymbolType(AffixPatternType type, UErrorCode& status) const {
473 return AffixUtils::containsType(posPrefix, type, status) ||
474 AffixUtils::containsType(posSuffix, type, status) ||
475 AffixUtils::containsType(negPrefix, type, status) ||
476 AffixUtils::containsType(negSuffix, type, status);
477 }
478
479 bool PropertiesAffixPatternProvider::hasBody() const {
480 return true;
481 }
482
483
484 void CurrencyPluralInfoAffixProvider::setTo(const CurrencyPluralInfo& cpi,
485 const DecimalFormatProperties& properties,
486 UErrorCode& status) {
487 // We need to use a PropertiesAffixPatternProvider, not the simpler version ParsedPatternInfo,
488 // because user-specified affix overrides still need to work.
489 fBogus = false;
490 DecimalFormatProperties pluralProperties(properties);
491 for (int32_t plural = 0; plural < StandardPlural::COUNT; plural++) {
492 const char* keyword = StandardPlural::getKeyword(static_cast<StandardPlural::Form>(plural));
493 UnicodeString patternString;
494 patternString = cpi.getCurrencyPluralPattern(keyword, patternString);
495 PatternParser::parseToExistingProperties(
496 patternString,
497 pluralProperties,
498 IGNORE_ROUNDING_NEVER,
499 status);
500 affixesByPlural[plural].setTo(pluralProperties, status);
501 }
502 }
503
504 char16_t CurrencyPluralInfoAffixProvider::charAt(int32_t flags, int32_t i) const {
505 int32_t pluralOrdinal = (flags & AFFIX_PLURAL_MASK);
506 return affixesByPlural[pluralOrdinal].charAt(flags, i);
507 }
508
509 int32_t CurrencyPluralInfoAffixProvider::length(int32_t flags) const {
510 int32_t pluralOrdinal = (flags & AFFIX_PLURAL_MASK);
511 return affixesByPlural[pluralOrdinal].length(flags);
512 }
513
514 UnicodeString CurrencyPluralInfoAffixProvider::getString(int32_t flags) const {
515 int32_t pluralOrdinal = (flags & AFFIX_PLURAL_MASK);
516 return affixesByPlural[pluralOrdinal].getString(flags);
517 }
518
519 bool CurrencyPluralInfoAffixProvider::positiveHasPlusSign() const {
520 return affixesByPlural[StandardPlural::OTHER].positiveHasPlusSign();
521 }
522
523 bool CurrencyPluralInfoAffixProvider::hasNegativeSubpattern() const {
524 return affixesByPlural[StandardPlural::OTHER].hasNegativeSubpattern();
525 }
526
527 bool CurrencyPluralInfoAffixProvider::negativeHasMinusSign() const {
528 return affixesByPlural[StandardPlural::OTHER].negativeHasMinusSign();
529 }
530
531 bool CurrencyPluralInfoAffixProvider::hasCurrencySign() const {
532 return affixesByPlural[StandardPlural::OTHER].hasCurrencySign();
533 }
534
535 bool CurrencyPluralInfoAffixProvider::containsSymbolType(AffixPatternType type, UErrorCode& status) const {
536 return affixesByPlural[StandardPlural::OTHER].containsSymbolType(type, status);
537 }
538
539 bool CurrencyPluralInfoAffixProvider::hasBody() const {
540 return affixesByPlural[StandardPlural::OTHER].hasBody();
541 }
542
543
544 #endif /* #if !UCONFIG_NO_FORMATTING */