mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-08 06:53:45 +00:00
ICU-10273 support plurals with decimals in MessageFormat and PluralFormat (ported from Java r34087 & r34276)
X-SVN-Rev: 34277
This commit is contained in:
parent
56d546f8c5
commit
fb38bbbee8
9 changed files with 380 additions and 101 deletions
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 1997-2013, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
* Copyright (C) 1997-2013, International Business Machines Corporation and
|
||||
* others. All Rights Reserved.
|
||||
*******************************************************************************
|
||||
*
|
||||
* File DECIMFMT.CPP
|
||||
|
@ -1035,6 +1035,7 @@ DecimalFormat::getFixedDecimal(double number, UErrorCode &status) const {
|
|||
return result;
|
||||
}
|
||||
|
||||
result.source = number;
|
||||
int32_t minFractionDigits = getMinimumFractionDigits();
|
||||
|
||||
if (fMultiplier == NULL && fScale == 0 && fRoundingIncrement == 0 && areSignificantDigitsUsed() == FALSE &&
|
||||
|
@ -1085,7 +1086,7 @@ DecimalFormat::getFixedDecimal(const Formattable &number, UErrorCode &status) co
|
|||
|
||||
DigitList *digits = number.getDigitList();
|
||||
if (digits == NULL || digits->getCount() <= 15) {
|
||||
return getFixedDecimal(number.getDouble(), status);
|
||||
return getFixedDecimal(number.getDouble(status), status);
|
||||
}
|
||||
|
||||
// We have an incoming DigitList in the formattable, and it holds more digits than
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
#include "patternprops.h"
|
||||
#include "messageimpl.h"
|
||||
#include "msgfmt_impl.h"
|
||||
#include "plurrule_impl.h"
|
||||
#include "uassert.h"
|
||||
#include "uelement.h"
|
||||
#include "uhash.h"
|
||||
|
@ -203,6 +204,16 @@ public:
|
|||
append(s);
|
||||
}
|
||||
}
|
||||
void formatAndAppend(const Format* formatter, const Formattable& arg,
|
||||
const UnicodeString &argString, UErrorCode& ec) {
|
||||
if (!argString.isEmpty()) {
|
||||
if (U_SUCCESS(ec)) {
|
||||
append(argString);
|
||||
}
|
||||
} else {
|
||||
formatAndAppend(formatter, arg, ec);
|
||||
}
|
||||
}
|
||||
int32_t length() {
|
||||
return len;
|
||||
}
|
||||
|
@ -229,8 +240,8 @@ MessageFormat::MessageFormat(const UnicodeString& pattern,
|
|||
defaultDateFormat(NULL),
|
||||
cachedFormatters(NULL),
|
||||
customFormatArgStarts(NULL),
|
||||
pluralProvider(&fLocale, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(&fLocale, UPLURAL_TYPE_ORDINAL)
|
||||
pluralProvider(*this, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(*this, UPLURAL_TYPE_ORDINAL)
|
||||
{
|
||||
setLocaleIDs(fLocale.getName(), fLocale.getName());
|
||||
applyPattern(pattern, success);
|
||||
|
@ -251,8 +262,8 @@ MessageFormat::MessageFormat(const UnicodeString& pattern,
|
|||
defaultDateFormat(NULL),
|
||||
cachedFormatters(NULL),
|
||||
customFormatArgStarts(NULL),
|
||||
pluralProvider(&fLocale, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(&fLocale, UPLURAL_TYPE_ORDINAL)
|
||||
pluralProvider(*this, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(*this, UPLURAL_TYPE_ORDINAL)
|
||||
{
|
||||
setLocaleIDs(fLocale.getName(), fLocale.getName());
|
||||
applyPattern(pattern, success);
|
||||
|
@ -274,8 +285,8 @@ MessageFormat::MessageFormat(const UnicodeString& pattern,
|
|||
defaultDateFormat(NULL),
|
||||
cachedFormatters(NULL),
|
||||
customFormatArgStarts(NULL),
|
||||
pluralProvider(&fLocale, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(&fLocale, UPLURAL_TYPE_ORDINAL)
|
||||
pluralProvider(*this, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(*this, UPLURAL_TYPE_ORDINAL)
|
||||
{
|
||||
setLocaleIDs(fLocale.getName(), fLocale.getName());
|
||||
applyPattern(pattern, parseError, success);
|
||||
|
@ -296,8 +307,8 @@ MessageFormat::MessageFormat(const MessageFormat& that)
|
|||
defaultDateFormat(NULL),
|
||||
cachedFormatters(NULL),
|
||||
customFormatArgStarts(NULL),
|
||||
pluralProvider(&fLocale, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(&fLocale, UPLURAL_TYPE_ORDINAL)
|
||||
pluralProvider(*this, UPLURAL_TYPE_CARDINAL),
|
||||
ordinalProvider(*this, UPLURAL_TYPE_ORDINAL)
|
||||
{
|
||||
// This will take care of creating the hash tables (since they are NULL).
|
||||
UErrorCode ec = U_ZERO_ERROR;
|
||||
|
@ -440,8 +451,8 @@ MessageFormat::setLocale(const Locale& theLocale)
|
|||
defaultDateFormat = NULL;
|
||||
fLocale = theLocale;
|
||||
setLocaleIDs(fLocale.getName(), fLocale.getName());
|
||||
pluralProvider.reset(&fLocale);
|
||||
ordinalProvider.reset(&fLocale);
|
||||
pluralProvider.reset();
|
||||
ordinalProvider.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -831,12 +842,7 @@ MessageFormat::getFormats(int32_t& cnt) const
|
|||
|
||||
UnicodeString MessageFormat::getArgName(int32_t partIndex) {
|
||||
const MessagePattern::Part& part = msgPattern.getPart(partIndex);
|
||||
if (part.getType() == UMSGPAT_PART_TYPE_ARG_NAME) {
|
||||
return msgPattern.getSubstring(part);
|
||||
} else {
|
||||
UnicodeString temp;
|
||||
return itos(part.getValue(), temp);
|
||||
}
|
||||
return msgPattern.getSubstring(part);
|
||||
}
|
||||
|
||||
StringEnumeration*
|
||||
|
@ -945,13 +951,55 @@ MessageFormat::format(const Formattable* arguments,
|
|||
|
||||
UnicodeStringAppendable usapp(appendTo);
|
||||
AppendableWrapper app(usapp);
|
||||
format(0, 0.0, arguments, argumentNames, cnt, app, pos, status);
|
||||
format(0, NULL, arguments, argumentNames, cnt, app, pos, status);
|
||||
return appendTo;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Mutable input/output values for the PluralSelectorProvider.
|
||||
* Separate so that it is possible to make MessageFormat Freezable.
|
||||
*/
|
||||
class PluralSelectorContext {
|
||||
public:
|
||||
PluralSelectorContext(int32_t start, const UnicodeString &name,
|
||||
const Formattable &num, double off, UErrorCode &errorCode)
|
||||
: startIndex(start), argName(name), offset(off),
|
||||
numberArgIndex(-1), formatter(NULL), forReplaceNumber(FALSE) {
|
||||
// number needs to be set even when select() is not called.
|
||||
// Keep it as a Number/Formattable:
|
||||
// For format() methods, and to preserve information (e.g., BigDecimal).
|
||||
if(off == 0) {
|
||||
number = num;
|
||||
} else {
|
||||
number = num.getDouble(errorCode) - off;
|
||||
}
|
||||
}
|
||||
|
||||
// Input values for plural selection with decimals.
|
||||
int32_t startIndex;
|
||||
const UnicodeString &argName;
|
||||
/** argument number - plural offset */
|
||||
Formattable number;
|
||||
double offset;
|
||||
// Output values for plural selection with decimals.
|
||||
/** -1 if REPLACE_NUMBER, 0 arg not found, >0 ARG_START index */
|
||||
int32_t numberArgIndex;
|
||||
const Format *formatter;
|
||||
/** formatted argument number - plural offset */
|
||||
UnicodeString numberString;
|
||||
/** TRUE if number-offset was formatted with the stock number formatter */
|
||||
UBool forReplaceNumber;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// if argumentNames is NULL, this means arguments is a numeric array.
|
||||
// arguments can not be NULL.
|
||||
void MessageFormat::format(int32_t msgStart, double pluralNumber,
|
||||
// We use const void *plNumber rather than const PluralSelectorContext *pluralNumber
|
||||
// so that we need not declare the PluralSelectorContext in the public header file.
|
||||
void MessageFormat::format(int32_t msgStart, const void *plNumber,
|
||||
const Formattable* arguments,
|
||||
const UnicodeString *argumentNames,
|
||||
int32_t cnt,
|
||||
|
@ -974,8 +1022,16 @@ void MessageFormat::format(int32_t msgStart, double pluralNumber,
|
|||
}
|
||||
prevIndex = part->getLimit();
|
||||
if (type == UMSGPAT_PART_TYPE_REPLACE_NUMBER) {
|
||||
const NumberFormat* nf = getDefaultNumberFormat(success);
|
||||
appendTo.formatAndAppend(nf, Formattable(pluralNumber), success);
|
||||
const PluralSelectorContext &pluralNumber =
|
||||
*static_cast<const PluralSelectorContext *>(plNumber);
|
||||
if(pluralNumber.forReplaceNumber) {
|
||||
// number-offset was already formatted.
|
||||
appendTo.formatAndAppend(pluralNumber.formatter,
|
||||
pluralNumber.number, pluralNumber.numberString, success);
|
||||
} else {
|
||||
const NumberFormat* nf = getDefaultNumberFormat(success);
|
||||
appendTo.formatAndAppend(nf, pluralNumber.number, success);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (type != UMSGPAT_PART_TYPE_ARG_START) {
|
||||
|
@ -985,38 +1041,43 @@ void MessageFormat::format(int32_t msgStart, double pluralNumber,
|
|||
UMessagePatternArgType argType = part->getArgType();
|
||||
part = &msgPattern.getPart(++i);
|
||||
const Formattable* arg;
|
||||
UnicodeString noArg;
|
||||
UBool noArg = FALSE;
|
||||
UnicodeString argName = msgPattern.getSubstring(*part);
|
||||
if (argumentNames == NULL) {
|
||||
int32_t argNumber = part->getValue(); // ARG_NUMBER
|
||||
if (0 <= argNumber && argNumber < cnt) {
|
||||
arg = arguments + argNumber;
|
||||
} else {
|
||||
arg = NULL;
|
||||
noArg.append(LEFT_CURLY_BRACE);
|
||||
itos(argNumber, noArg);
|
||||
noArg.append(RIGHT_CURLY_BRACE);
|
||||
noArg = TRUE;
|
||||
}
|
||||
} else {
|
||||
UnicodeString key;
|
||||
if (part->getType() == UMSGPAT_PART_TYPE_ARG_NAME) {
|
||||
key = msgPattern.getSubstring(*part);
|
||||
} else /* UMSGPAT_PART_TYPE_ARG_NUMBER */ {
|
||||
itos(part->getValue(), key);
|
||||
}
|
||||
arg = getArgFromListByName(arguments, argumentNames, cnt, key);
|
||||
arg = getArgFromListByName(arguments, argumentNames, cnt, argName);
|
||||
if (arg == NULL) {
|
||||
noArg.append(LEFT_CURLY_BRACE);
|
||||
noArg.append(key);
|
||||
noArg.append(RIGHT_CURLY_BRACE);
|
||||
noArg = TRUE;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
int32_t prevDestLength = appendTo.length();
|
||||
const Format* formatter = NULL;
|
||||
if (!noArg.isEmpty()) {
|
||||
appendTo.append(noArg);
|
||||
if (noArg) {
|
||||
appendTo.append(
|
||||
UnicodeString(LEFT_CURLY_BRACE).append(argName).append(RIGHT_CURLY_BRACE));
|
||||
} else if (arg == NULL) {
|
||||
appendTo.append(NULL_STRING, 4);
|
||||
} else if(plNumber!=NULL &&
|
||||
static_cast<const PluralSelectorContext *>(plNumber)->numberArgIndex==(i-2)) {
|
||||
const PluralSelectorContext &pluralNumber =
|
||||
*static_cast<const PluralSelectorContext *>(plNumber);
|
||||
if(pluralNumber.offset == 0) {
|
||||
// The number was already formatted with this formatter.
|
||||
appendTo.formatAndAppend(pluralNumber.formatter, pluralNumber.number,
|
||||
pluralNumber.numberString, success);
|
||||
} else {
|
||||
// Do not use the formatted (number-offset) string for a named argument
|
||||
// that formats the number without subtracting the offset.
|
||||
appendTo.formatAndAppend(pluralNumber.formatter, *arg, success);
|
||||
}
|
||||
} else if ((formatter = getCachedFormatter(i -2))) {
|
||||
// Handles all ArgType.SIMPLE, and formatters from setFormat() and its siblings.
|
||||
if (dynamic_cast<const ChoiceFormat*>(formatter) ||
|
||||
|
@ -1031,7 +1092,7 @@ void MessageFormat::format(int32_t msgStart, double pluralNumber,
|
|||
(subMsgString.indexOf(SINGLE_QUOTE) >= 0 && !MessageImpl::jdkAposMode(msgPattern))
|
||||
) {
|
||||
MessageFormat subMsgFormat(subMsgString, fLocale, success);
|
||||
subMsgFormat.format(0, 0, arguments, argumentNames, cnt, appendTo, ignore, success);
|
||||
subMsgFormat.format(0, NULL, arguments, argumentNames, cnt, appendTo, ignore, success);
|
||||
} else {
|
||||
appendTo.append(subMsgString);
|
||||
}
|
||||
|
@ -1060,26 +1121,26 @@ void MessageFormat::format(int32_t msgStart, double pluralNumber,
|
|||
// because only this one converts non-double numeric types to double.
|
||||
const double number = arg->getDouble(success);
|
||||
int32_t subMsgStart = ChoiceFormat::findSubMessage(msgPattern, i, number);
|
||||
formatComplexSubMessage(subMsgStart, 0, arguments, argumentNames,
|
||||
formatComplexSubMessage(subMsgStart, NULL, arguments, argumentNames,
|
||||
cnt, appendTo, success);
|
||||
} else if (UMSGPAT_ARG_TYPE_HAS_PLURAL_STYLE(argType)) {
|
||||
if (!arg->isNumeric()) {
|
||||
success = U_ILLEGAL_ARGUMENT_ERROR;
|
||||
return;
|
||||
}
|
||||
const PluralFormat::PluralSelector &selector =
|
||||
const PluralSelectorProvider &selector =
|
||||
argType == UMSGPAT_ARG_TYPE_PLURAL ? pluralProvider : ordinalProvider;
|
||||
// We must use the Formattable::getDouble() variant with the UErrorCode parameter
|
||||
// because only this one converts non-double numeric types to double.
|
||||
double number = arg->getDouble(success);
|
||||
int32_t subMsgStart = PluralFormat::findSubMessage(msgPattern, i, selector, number,
|
||||
success);
|
||||
double offset = msgPattern.getPluralOffset(i);
|
||||
formatComplexSubMessage(subMsgStart, number-offset, arguments, argumentNames,
|
||||
PluralSelectorContext context(i, argName, *arg, offset, success);
|
||||
int32_t subMsgStart = PluralFormat::findSubMessage(
|
||||
msgPattern, i, selector, &context, arg->getDouble(success), success);
|
||||
formatComplexSubMessage(subMsgStart, &context, arguments, argumentNames,
|
||||
cnt, appendTo, success);
|
||||
} else if (argType == UMSGPAT_ARG_TYPE_SELECT) {
|
||||
int32_t subMsgStart = SelectFormat::findSubMessage(msgPattern, i, arg->getString(success), success);
|
||||
formatComplexSubMessage(subMsgStart, 0, arguments, argumentNames,
|
||||
formatComplexSubMessage(subMsgStart, NULL, arguments, argumentNames,
|
||||
cnt, appendTo, success);
|
||||
} else {
|
||||
// This should never happen.
|
||||
|
@ -1094,7 +1155,7 @@ void MessageFormat::format(int32_t msgStart, double pluralNumber,
|
|||
|
||||
|
||||
void MessageFormat::formatComplexSubMessage(int32_t msgStart,
|
||||
double pluralNumber,
|
||||
const void *plNumber,
|
||||
const Formattable* arguments,
|
||||
const UnicodeString *argumentNames,
|
||||
int32_t cnt,
|
||||
|
@ -1105,7 +1166,7 @@ void MessageFormat::formatComplexSubMessage(int32_t msgStart,
|
|||
}
|
||||
|
||||
if (!MessageImpl::jdkAposMode(msgPattern)) {
|
||||
format(msgStart, pluralNumber, arguments, argumentNames, cnt, appendTo, NULL, success);
|
||||
format(msgStart, plNumber, arguments, argumentNames, cnt, appendTo, NULL, success);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1127,8 +1188,15 @@ void MessageFormat::formatComplexSubMessage(int32_t msgStart,
|
|||
} else if (type == UMSGPAT_PART_TYPE_REPLACE_NUMBER || type == UMSGPAT_PART_TYPE_SKIP_SYNTAX) {
|
||||
sb.append(msgString, prevIndex, index - prevIndex);
|
||||
if (type == UMSGPAT_PART_TYPE_REPLACE_NUMBER) {
|
||||
const NumberFormat* nf = getDefaultNumberFormat(success);
|
||||
sb.append(nf->format(pluralNumber, sb, success));
|
||||
const PluralSelectorContext &pluralNumber =
|
||||
*static_cast<const PluralSelectorContext *>(plNumber);
|
||||
if(pluralNumber.forReplaceNumber) {
|
||||
// number-offset was already formatted.
|
||||
sb.append(pluralNumber.numberString);
|
||||
} else {
|
||||
const NumberFormat* nf = getDefaultNumberFormat(success);
|
||||
sb.append(nf->format(pluralNumber.number, sb, success));
|
||||
}
|
||||
}
|
||||
prevIndex = part.getLimit();
|
||||
} else if (type == UMSGPAT_PART_TYPE_ARG_START) {
|
||||
|
@ -1144,7 +1212,7 @@ void MessageFormat::formatComplexSubMessage(int32_t msgStart,
|
|||
UnicodeString emptyPattern; // gcc 3.3.3 fails with "UnicodeString()" as the first parameter.
|
||||
MessageFormat subMsgFormat(emptyPattern, fLocale, success);
|
||||
subMsgFormat.applyPattern(sb, UMSGPAT_APOS_DOUBLE_REQUIRED, NULL, success);
|
||||
subMsgFormat.format(0, 0, arguments, argumentNames, cnt, appendTo, NULL, success);
|
||||
subMsgFormat.format(0, NULL, arguments, argumentNames, cnt, appendTo, NULL, success);
|
||||
} else {
|
||||
appendTo.append(sb);
|
||||
}
|
||||
|
@ -1184,6 +1252,59 @@ FieldPosition* MessageFormat::updateMetaData(AppendableWrapper& /*dest*/, int32_
|
|||
*/
|
||||
}
|
||||
|
||||
int32_t
|
||||
MessageFormat::findOtherSubMessage(int32_t partIndex) const {
|
||||
int32_t count=msgPattern.countParts();
|
||||
const MessagePattern::Part *part = &msgPattern.getPart(partIndex);
|
||||
if(MessagePattern::Part::hasNumericValue(part->getType())) {
|
||||
++partIndex;
|
||||
}
|
||||
// Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples
|
||||
// until ARG_LIMIT or end of plural-only pattern.
|
||||
UnicodeString other(FALSE, OTHER_STRING, 5);
|
||||
do {
|
||||
part=&msgPattern.getPart(partIndex++);
|
||||
UMessagePatternPartType type=part->getType();
|
||||
if(type==UMSGPAT_PART_TYPE_ARG_LIMIT) {
|
||||
break;
|
||||
}
|
||||
U_ASSERT(type==UMSGPAT_PART_TYPE_ARG_SELECTOR);
|
||||
// part is an ARG_SELECTOR followed by an optional explicit value, and then a message
|
||||
if(msgPattern.partSubstringMatches(*part, other)) {
|
||||
return partIndex;
|
||||
}
|
||||
if(MessagePattern::Part::hasNumericValue(msgPattern.getPartType(partIndex))) {
|
||||
++partIndex; // skip the numeric-value part of "=1" etc.
|
||||
}
|
||||
partIndex=msgPattern.getLimitPartIndex(partIndex);
|
||||
} while(++partIndex<count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t
|
||||
MessageFormat::findFirstPluralNumberArg(int32_t msgStart, const UnicodeString &argName) const {
|
||||
for(int32_t i=msgStart+1;; ++i) {
|
||||
const MessagePattern::Part &part=msgPattern.getPart(i);
|
||||
UMessagePatternPartType type=part.getType();
|
||||
if(type==UMSGPAT_PART_TYPE_MSG_LIMIT) {
|
||||
return 0;
|
||||
}
|
||||
if(type==UMSGPAT_PART_TYPE_REPLACE_NUMBER) {
|
||||
return -1;
|
||||
}
|
||||
if(type==UMSGPAT_PART_TYPE_ARG_START) {
|
||||
UMessagePatternArgType argType=part.getArgType();
|
||||
if(!argName.isEmpty() && (argType==UMSGPAT_ARG_TYPE_NONE || argType==UMSGPAT_ARG_TYPE_SIMPLE)) {
|
||||
// ARG_NUMBER or ARG_NAME
|
||||
if(msgPattern.partSubstringMatches(msgPattern.getPart(i+1), argName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
i=msgPattern.getLimitPartIndex(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageFormat::copyObjects(const MessageFormat& that, UErrorCode& ec) {
|
||||
// Deep copy pointer fields.
|
||||
// We need not copy the formatAliases because they are re-filled
|
||||
|
@ -1797,32 +1918,55 @@ FormatNameEnumeration::~FormatNameEnumeration() {
|
|||
delete fFormatNames;
|
||||
}
|
||||
|
||||
|
||||
MessageFormat::PluralSelectorProvider::PluralSelectorProvider(const Locale* loc, UPluralType t)
|
||||
: locale(loc), rules(NULL), type(t) {
|
||||
MessageFormat::PluralSelectorProvider::PluralSelectorProvider(const MessageFormat &mf, UPluralType t)
|
||||
: msgFormat(mf), rules(NULL), type(t) {
|
||||
}
|
||||
|
||||
MessageFormat::PluralSelectorProvider::~PluralSelectorProvider() {
|
||||
// We own the rules but not the locale.
|
||||
delete rules;
|
||||
}
|
||||
|
||||
UnicodeString MessageFormat::PluralSelectorProvider::select(double number, UErrorCode& ec) const {
|
||||
UnicodeString MessageFormat::PluralSelectorProvider::select(void *ctx, double number,
|
||||
UErrorCode& ec) const {
|
||||
if (U_FAILURE(ec)) {
|
||||
return UnicodeString(FALSE, OTHER_STRING, 5);
|
||||
}
|
||||
MessageFormat::PluralSelectorProvider* t = const_cast<MessageFormat::PluralSelectorProvider*>(this);
|
||||
if(rules == NULL) {
|
||||
t->rules = PluralRules::forLocale(*locale, type, ec);
|
||||
t->rules = PluralRules::forLocale(msgFormat.fLocale, type, ec);
|
||||
if (U_FAILURE(ec)) {
|
||||
return UnicodeString(FALSE, OTHER_STRING, 5);
|
||||
}
|
||||
}
|
||||
return rules->select(number);
|
||||
// Select a sub-message according to how the number is formatted,
|
||||
// which is specified in the selected sub-message.
|
||||
// We avoid this circle by looking at how
|
||||
// the number is formatted in the "other" sub-message
|
||||
// which must always be present and usually contains the number.
|
||||
// Message authors should be consistent across sub-messages.
|
||||
PluralSelectorContext &context = *static_cast<PluralSelectorContext *>(ctx);
|
||||
int32_t otherIndex = msgFormat.findOtherSubMessage(context.startIndex);
|
||||
context.numberArgIndex = msgFormat.findFirstPluralNumberArg(otherIndex, context.argName);
|
||||
if(context.numberArgIndex > 0 && msgFormat.cachedFormatters != NULL) {
|
||||
context.formatter =
|
||||
(const Format*)uhash_iget(msgFormat.cachedFormatters, context.numberArgIndex);
|
||||
}
|
||||
if(context.formatter == NULL) {
|
||||
context.formatter = msgFormat.getDefaultNumberFormat(ec);
|
||||
context.forReplaceNumber = TRUE;
|
||||
}
|
||||
U_ASSERT(context.number.getDouble(ec) == number); // argument number minus the offset
|
||||
context.formatter->format(context.number, context.numberString, ec);
|
||||
const DecimalFormat *decFmt = dynamic_cast<const DecimalFormat *>(context.formatter);
|
||||
if(decFmt != NULL) {
|
||||
FixedDecimal dec = decFmt->getFixedDecimal(context.number, ec);
|
||||
return rules->select(dec);
|
||||
} else {
|
||||
return rules->select(number);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageFormat::PluralSelectorProvider::reset(const Locale* loc) {
|
||||
locale = loc;
|
||||
void MessageFormat::PluralSelectorProvider::reset() {
|
||||
delete rules;
|
||||
rules = NULL;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2009-2012, International Business Machines Corporation and
|
||||
* Copyright (C) 2009-2013, International Business Machines Corporation and
|
||||
* others. All Rights Reserved.
|
||||
*******************************************************************************
|
||||
*
|
||||
* File PLURFMT.CPP
|
||||
*
|
||||
* Modification History:
|
||||
*
|
||||
* Date Name Description
|
||||
*******************************************************************************
|
||||
*/
|
||||
|
||||
#include "unicode/decimfmt.h"
|
||||
#include "unicode/messagepattern.h"
|
||||
#include "unicode/plurfmt.h"
|
||||
#include "unicode/plurrule.h"
|
||||
|
@ -207,7 +204,7 @@ PluralFormat::format(const Formattable& obj,
|
|||
if (U_FAILURE(status)) return appendTo;
|
||||
|
||||
if (obj.isNumeric()) {
|
||||
return format(obj.getDouble(), appendTo, pos, status);
|
||||
return format(obj, obj.getDouble(), appendTo, pos, status);
|
||||
} else {
|
||||
status = U_ILLEGAL_ARGUMENT_ERROR;
|
||||
return appendTo;
|
||||
|
@ -218,14 +215,14 @@ UnicodeString
|
|||
PluralFormat::format(int32_t number, UErrorCode& status) const {
|
||||
FieldPosition fpos(0);
|
||||
UnicodeString result;
|
||||
return format(number, result, fpos, status);
|
||||
return format(Formattable(number), number, result, fpos, status);
|
||||
}
|
||||
|
||||
UnicodeString
|
||||
PluralFormat::format(double number, UErrorCode& status) const {
|
||||
FieldPosition fpos(0);
|
||||
UnicodeString result;
|
||||
return format(number, result, fpos, status);
|
||||
return format(Formattable(number), number, result, fpos, status);
|
||||
}
|
||||
|
||||
|
||||
|
@ -234,7 +231,7 @@ PluralFormat::format(int32_t number,
|
|||
UnicodeString& appendTo,
|
||||
FieldPosition& pos,
|
||||
UErrorCode& status) const {
|
||||
return format((double)number, appendTo, pos, status);
|
||||
return format(Formattable(number), (double)number, appendTo, pos, status);
|
||||
}
|
||||
|
||||
UnicodeString&
|
||||
|
@ -242,18 +239,44 @@ PluralFormat::format(double number,
|
|||
UnicodeString& appendTo,
|
||||
FieldPosition& pos,
|
||||
UErrorCode& status) const {
|
||||
return format(Formattable(number), (double)number, appendTo, pos, status);
|
||||
}
|
||||
|
||||
UnicodeString&
|
||||
PluralFormat::format(const Formattable& numberObject, double number,
|
||||
UnicodeString& appendTo,
|
||||
FieldPosition& pos,
|
||||
UErrorCode& status) const {
|
||||
if (U_FAILURE(status)) {
|
||||
return appendTo;
|
||||
}
|
||||
if (msgPattern.countParts() == 0) {
|
||||
return numberFormat->format(number, appendTo, pos);
|
||||
return numberFormat->format(numberObject, appendTo, pos, status);
|
||||
}
|
||||
// Get the appropriate sub-message.
|
||||
int32_t partIndex = findSubMessage(msgPattern, 0, pluralRulesWrapper, number, status);
|
||||
// Select it based on the formatted number-offset.
|
||||
double numberMinusOffset = number - offset;
|
||||
UnicodeString numberString;
|
||||
FieldPosition ignorePos;
|
||||
FixedDecimal dec(numberMinusOffset);
|
||||
if (offset == 0) {
|
||||
numberFormat->format(numberObject, numberString, ignorePos, status); // could be BigDecimal etc.
|
||||
DecimalFormat *decFmt = dynamic_cast<DecimalFormat *>(numberFormat);
|
||||
if(decFmt != NULL) {
|
||||
dec = decFmt->getFixedDecimal(numberObject, status);
|
||||
}
|
||||
} else {
|
||||
numberFormat->format(numberMinusOffset, numberString, ignorePos, status);
|
||||
DecimalFormat *decFmt = dynamic_cast<DecimalFormat *>(numberFormat);
|
||||
if(decFmt != NULL) {
|
||||
dec = decFmt->getFixedDecimal(numberMinusOffset, status);
|
||||
}
|
||||
}
|
||||
int32_t partIndex = findSubMessage(msgPattern, 0, pluralRulesWrapper, &dec, number, status);
|
||||
if (U_FAILURE(status)) { return appendTo; }
|
||||
// Replace syntactic # signs in the top level of this sub-message
|
||||
// (not in nested arguments) with the formatted number-offset.
|
||||
const UnicodeString& pattern = msgPattern.getPatternString();
|
||||
number -= offset;
|
||||
int32_t prevIndex = msgPattern.getPart(partIndex).getLimit();
|
||||
for (;;) {
|
||||
const MessagePattern::Part& part = msgPattern.getPart(++partIndex);
|
||||
|
@ -265,7 +288,7 @@ PluralFormat::format(double number,
|
|||
(type == UMSGPAT_PART_TYPE_SKIP_SYNTAX && MessageImpl::jdkAposMode(msgPattern))) {
|
||||
appendTo.append(pattern, prevIndex, index - prevIndex);
|
||||
if (type == UMSGPAT_PART_TYPE_REPLACE_NUMBER) {
|
||||
numberFormat->format(number, appendTo);
|
||||
appendTo.append(numberString);
|
||||
}
|
||||
prevIndex = part.getLimit();
|
||||
} else if (type == UMSGPAT_PART_TYPE_ARG_START) {
|
||||
|
@ -370,7 +393,8 @@ PluralFormat::parseObject(const UnicodeString& /*source*/,
|
|||
}
|
||||
|
||||
int32_t PluralFormat::findSubMessage(const MessagePattern& pattern, int32_t partIndex,
|
||||
const PluralSelector& selector, double number, UErrorCode& ec) {
|
||||
const PluralSelector& selector, void *context,
|
||||
double number, UErrorCode& ec) {
|
||||
if (U_FAILURE(ec)) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -383,7 +407,7 @@ int32_t PluralFormat::findSubMessage(const MessagePattern& pattern, int32_t part
|
|||
} else {
|
||||
offset=0;
|
||||
}
|
||||
// The keyword is empty until we need to match against non-explicit, not-"other" value.
|
||||
// The keyword is empty until we need to match against a non-explicit, not-"other" value.
|
||||
// Then we get the keyword from the selector.
|
||||
// (In other words, we never call the selector if we match against an explicit value,
|
||||
// or if the only non-explicit keyword is "other".)
|
||||
|
@ -436,7 +460,7 @@ int32_t PluralFormat::findSubMessage(const MessagePattern& pattern, int32_t part
|
|||
}
|
||||
} else {
|
||||
if(keyword.isEmpty()) {
|
||||
keyword=selector.select(number-offset, ec);
|
||||
keyword=selector.select(context, number-offset, ec);
|
||||
if(msgStart!=0 && (0 == keyword.compare(other))) {
|
||||
// We have already seen an "other" sub-message.
|
||||
// Do not match "other" again.
|
||||
|
@ -463,9 +487,11 @@ PluralFormat::PluralSelectorAdapter::~PluralSelectorAdapter() {
|
|||
delete pluralRules;
|
||||
}
|
||||
|
||||
UnicodeString PluralFormat::PluralSelectorAdapter::select(double number,
|
||||
UnicodeString PluralFormat::PluralSelectorAdapter::select(void *context, double number,
|
||||
UErrorCode& /*ec*/) const {
|
||||
return pluralRules->select(number);
|
||||
FixedDecimal *dec=static_cast<FixedDecimal *>(context);
|
||||
U_ASSERT(dec->source==number);
|
||||
return pluralRules->select(*dec);
|
||||
}
|
||||
|
||||
void PluralFormat::PluralSelectorAdapter::reset() {
|
||||
|
|
|
@ -874,13 +874,13 @@ private:
|
|||
*/
|
||||
class U_I18N_API PluralSelectorProvider : public PluralFormat::PluralSelector {
|
||||
public:
|
||||
PluralSelectorProvider(const Locale* loc, UPluralType type);
|
||||
PluralSelectorProvider(const MessageFormat &mf, UPluralType type);
|
||||
virtual ~PluralSelectorProvider();
|
||||
virtual UnicodeString select(double number, UErrorCode& ec) const;
|
||||
virtual UnicodeString select(void *ctx, double number, UErrorCode& ec) const;
|
||||
|
||||
void reset(const Locale* loc);
|
||||
void reset();
|
||||
private:
|
||||
const Locale* locale;
|
||||
const MessageFormat &msgFormat;
|
||||
PluralRules* rules;
|
||||
UPluralType type;
|
||||
};
|
||||
|
@ -956,7 +956,7 @@ private:
|
|||
* AppendableWrapper, updates the field position.
|
||||
*
|
||||
* @param msgStart Index to msgPattern part to start formatting from.
|
||||
* @param pluralNumber Zero except when formatting a plural argument sub-message
|
||||
* @param plNumber NULL except when formatting a plural argument sub-message
|
||||
* where a '#' is replaced by the format string for this number.
|
||||
* @param arguments The formattable objects array. (Must not be NULL.)
|
||||
* @param argumentNames NULL if numbered values are used. Otherwise the same
|
||||
|
@ -969,7 +969,7 @@ private:
|
|||
* @param success The error code status.
|
||||
*/
|
||||
void format(int32_t msgStart,
|
||||
double pluralNumber,
|
||||
const void *plNumber,
|
||||
const Formattable* arguments,
|
||||
const UnicodeString *argumentNames,
|
||||
int32_t cnt,
|
||||
|
@ -1008,6 +1008,20 @@ private:
|
|||
FieldPosition* updateMetaData(AppendableWrapper& dest, int32_t prevLength,
|
||||
FieldPosition* fp, const Formattable* argId) const;
|
||||
|
||||
/**
|
||||
* Finds the "other" sub-message.
|
||||
* @param partIndex the index of the first PluralFormat argument style part.
|
||||
* @return the "other" sub-message start part index.
|
||||
*/
|
||||
int32_t findOtherSubMessage(int32_t partIndex) const;
|
||||
|
||||
/**
|
||||
* Returns the ARG_START index of the first occurrence of the plural number in a sub-message.
|
||||
* Returns -1 if it is a REPLACE_NUMBER.
|
||||
* Returns 0 if there is neither.
|
||||
*/
|
||||
int32_t findFirstPluralNumberArg(int32_t msgStart, const UnicodeString &argName) const;
|
||||
|
||||
Format* getCachedFormatter(int32_t argumentNumber) const;
|
||||
|
||||
UnicodeString getLiteralStringUntilNextArgument(int32_t from) const;
|
||||
|
@ -1015,7 +1029,7 @@ private:
|
|||
void copyObjects(const MessageFormat& that, UErrorCode& ec);
|
||||
|
||||
void formatComplexSubMessage(int32_t msgStart,
|
||||
double pluralNumber,
|
||||
const void *plNumber,
|
||||
const Formattable* arguments,
|
||||
const UnicodeString *argumentNames,
|
||||
int32_t cnt,
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
*
|
||||
|
||||
* File PLURFMT.H
|
||||
*
|
||||
* Modification History:*
|
||||
* Date Name Description
|
||||
*
|
||||
********************************************************************************
|
||||
*/
|
||||
|
||||
|
@ -543,11 +539,12 @@ private:
|
|||
/**
|
||||
* Given a number, returns the appropriate PluralFormat keyword.
|
||||
*
|
||||
* @param context worker object for the selector.
|
||||
* @param number The number to be plural-formatted.
|
||||
* @param ec Error code.
|
||||
* @return The selected PluralFormat keyword.
|
||||
*/
|
||||
virtual UnicodeString select(double number, UErrorCode& ec) const = 0;
|
||||
virtual UnicodeString select(void *context, double number, UErrorCode& ec) const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -560,7 +557,7 @@ private:
|
|||
|
||||
virtual ~PluralSelectorAdapter();
|
||||
|
||||
virtual UnicodeString select(double number, UErrorCode& /*ec*/) const;
|
||||
virtual UnicodeString select(void *context, double number, UErrorCode& /*ec*/) const;
|
||||
|
||||
void reset();
|
||||
|
||||
|
@ -585,11 +582,17 @@ private:
|
|||
*/
|
||||
void copyObjects(const PluralFormat& other);
|
||||
|
||||
UnicodeString& format(const Formattable& numberObject, double number,
|
||||
UnicodeString& appendTo,
|
||||
FieldPosition& pos,
|
||||
UErrorCode& status) const;
|
||||
|
||||
/**
|
||||
* Finds the PluralFormat sub-message for the given number, or the "other" sub-message.
|
||||
* @param pattern A MessagePattern.
|
||||
* @param partIndex the index of the first PluralFormat argument style part.
|
||||
* @param selector the PluralSelector for mapping the number (minus offset) to a keyword.
|
||||
* @param context worker object for the selector.
|
||||
* @param number a number to be matched to one of the PluralFormat argument's explicit values,
|
||||
* or mapped via the PluralSelector.
|
||||
* @param ec ICU error code.
|
||||
|
@ -597,7 +600,7 @@ private:
|
|||
*/
|
||||
static int32_t findSubMessage(
|
||||
const MessagePattern& pattern, int32_t partIndex,
|
||||
const PluralSelector& selector, double number, UErrorCode& ec);
|
||||
const PluralSelector& selector, void *context, double number, UErrorCode& ec);
|
||||
|
||||
friend class MessageFormat;
|
||||
};
|
||||
|
|
|
@ -8,12 +8,14 @@
|
|||
|
||||
#if !UCONFIG_NO_FORMATTING
|
||||
|
||||
#include "plurults.h"
|
||||
#include "plurfmts.h"
|
||||
#include "cmemory.h"
|
||||
#include "unicode/dcfmtsym.h"
|
||||
#include "unicode/decimfmt.h"
|
||||
#include "unicode/msgfmt.h"
|
||||
#include "unicode/plurrule.h"
|
||||
#include "unicode/plurfmt.h"
|
||||
#include "unicode/plurrule.h"
|
||||
#include "cmemory.h"
|
||||
#include "plurfmts.h"
|
||||
#include "plurults.h"
|
||||
|
||||
#define PLURAL_PATTERN_DATA 4
|
||||
#define PLURAL_TEST_ARRAY_SIZE 256
|
||||
|
@ -38,6 +40,7 @@ void PluralFormatTest::runIndexedTest( int32_t index, UBool exec, const char* &n
|
|||
TESTCASE_AUTO(pluralFormatExtendedTest);
|
||||
TESTCASE_AUTO(pluralFormatExtendedParseTest);
|
||||
TESTCASE_AUTO(ordinalFormatTest);
|
||||
TESTCASE_AUTO(TestDecimals);
|
||||
TESTCASE_AUTO_END;
|
||||
}
|
||||
|
||||
|
@ -567,7 +570,7 @@ PluralFormatTest::pluralFormatExtendedTest(void) {
|
|||
dataerrln("Failed to apply pattern - %s", u_errorName(status));
|
||||
return;
|
||||
}
|
||||
for (int32_t i = 0; i < 7; ++i) {
|
||||
for (int32_t i = 0; i <= 7; ++i) {
|
||||
UnicodeString result = pf.format(i, status);
|
||||
if (U_FAILURE(status)) {
|
||||
errln("PluralFormat.format(value %d) failed - %s", i, u_errorName(status));
|
||||
|
@ -662,6 +665,22 @@ PluralFormatTest::ordinalFormatTest(void) {
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
PluralFormatTest::TestDecimals() {
|
||||
IcuTestErrorCode errorCode(*this, "TestDecimals");
|
||||
// Simple number replacement.
|
||||
PluralFormat pf(Locale::getEnglish(), "one{one meter}other{# meters}", errorCode);
|
||||
assertEquals("simple format(1)", "one meter", pf.format(1, errorCode));
|
||||
assertEquals("simple format(1.5)", "1.5 meters", pf.format(1.5, errorCode));
|
||||
PluralFormat pf2(Locale::getEnglish(),
|
||||
"offset:1 one{another meter}other{another # meters}", errorCode);
|
||||
DecimalFormat df("0.0", new DecimalFormatSymbols(Locale::getEnglish(), errorCode), errorCode);
|
||||
pf2.setNumberFormat(&df, errorCode);
|
||||
assertEquals("offset-decimals format(1)", "another 0.0 meters", pf2.format(1, errorCode));
|
||||
assertEquals("offset-decimals format(2)", "another 1.0 meters", pf2.format(2, errorCode));
|
||||
assertEquals("offset-decimals format(2.5)", "another 1.5 meters", pf2.format(2.5, errorCode));
|
||||
}
|
||||
|
||||
void
|
||||
PluralFormatTest::numberFormatTest(PluralFormat* plFmt,
|
||||
NumberFormat *numFmt,
|
||||
|
@ -707,7 +726,6 @@ PluralFormatTest::numberFormatTest(PluralFormat* plFmt,
|
|||
}
|
||||
else {
|
||||
errln( *message+UnicodeString(" got:")+plResult+UnicodeString(" expecting:")+numResult);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/********************************************************************
|
||||
* COPYRIGHT:
|
||||
* Copyright (c) 1997-2012, International Business Machines Corporation and
|
||||
* Copyright (c) 1997-2013, International Business Machines Corporation and
|
||||
* others. All Rights Reserved.
|
||||
********************************************************************/
|
||||
|
||||
|
@ -32,6 +32,7 @@ private:
|
|||
void pluralFormatExtendedTest();
|
||||
void pluralFormatExtendedParseTest();
|
||||
void ordinalFormatTest();
|
||||
void TestDecimals();
|
||||
void numberFormatTest(PluralFormat* plFmt,
|
||||
NumberFormat *numFmt,
|
||||
int32_t start,
|
||||
|
|
|
@ -67,6 +67,7 @@ TestMessageFormat::runIndexedTest(int32_t index, UBool exec,
|
|||
TESTCASE_AUTO(testGetFormatNames);
|
||||
TESTCASE_AUTO(TestTrimArgumentName);
|
||||
TESTCASE_AUTO(TestSelectOrdinal);
|
||||
TESTCASE_AUTO(TestDecimals);
|
||||
TESTCASE_AUTO_END;
|
||||
}
|
||||
|
||||
|
@ -1892,4 +1893,74 @@ void TestMessageFormat::TestSelectOrdinal() {
|
|||
errorCode.logDataIfFailureAndReset("");
|
||||
}
|
||||
|
||||
void TestMessageFormat::TestDecimals() {
|
||||
IcuTestErrorCode errorCode(*this, "TestDecimals");
|
||||
// Simple number replacement.
|
||||
MessageFormat m(
|
||||
"{0,plural,one{one meter}other{# meters}}",
|
||||
Locale::getEnglish(), errorCode);
|
||||
Formattable args[1] = { (int32_t)1 };
|
||||
FieldPosition ignore;
|
||||
UnicodeString result;
|
||||
assertEquals("simple format(1)", "one meter",
|
||||
m.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
args[0] = 1.5;
|
||||
result.remove();
|
||||
assertEquals("simple format(1.5)", "1.5 meters",
|
||||
m.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
// Simple but explicit.
|
||||
MessageFormat m0(
|
||||
"{0,plural,one{one meter}other{{0} meters}}",
|
||||
Locale::getEnglish(), errorCode);
|
||||
args[0] = 1;
|
||||
result.remove();
|
||||
assertEquals("explicit format(1)", "one meter",
|
||||
m0.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
args[0] = 1.5;
|
||||
result.remove();
|
||||
assertEquals("explicit format(1.5)", "1.5 meters",
|
||||
m0.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
// With offset and specific simple format with optional decimals.
|
||||
MessageFormat m1(
|
||||
"{0,plural,offset:1 one{another meter}other{{0,number,00.#} meters}}",
|
||||
Locale::getEnglish(), errorCode);
|
||||
args[0] = 1;
|
||||
result.remove();
|
||||
assertEquals("offset format(1)", "01 meters",
|
||||
m1.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
args[0] = 2;
|
||||
result.remove();
|
||||
assertEquals("offset format(1)", "another meter",
|
||||
m1.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
args[0] = 2.5;
|
||||
result.remove();
|
||||
assertEquals("offset format(1)", "02.5 meters",
|
||||
m1.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
// With offset and specific simple format with forced decimals.
|
||||
MessageFormat m2(
|
||||
"{0,plural,offset:1 one{another meter}other{{0,number,0.0} meters}}",
|
||||
Locale::getEnglish(), errorCode);
|
||||
args[0] = 1;
|
||||
result.remove();
|
||||
assertEquals("offset-decimals format(1)", "1.0 meters",
|
||||
m2.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
args[0] = 2;
|
||||
result.remove();
|
||||
assertEquals("offset-decimals format(1)", "2.0 meters",
|
||||
m2.format(args, 1, result, ignore, errorCode));
|
||||
|
||||
args[0] = 2.5;
|
||||
result.remove();
|
||||
assertEquals("offset-decimals format(1)", "2.5 meters",
|
||||
m2.format(args, 1, result, ignore, errorCode));
|
||||
}
|
||||
|
||||
#endif /* #if !UCONFIG_NO_FORMATTING */
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/********************************************************************
|
||||
* COPYRIGHT:
|
||||
* Copyright (c) 1997-2012, International Business Machines Corporation and
|
||||
* Copyright (c) 1997-2013, International Business Machines Corporation and
|
||||
* others. All Rights Reserved.
|
||||
********************************************************************/
|
||||
#ifndef _TESTMESSAGEFORMAT
|
||||
|
@ -117,6 +117,7 @@ public:
|
|||
void testGetFormatNames();
|
||||
void TestTrimArgumentName();
|
||||
void TestSelectOrdinal();
|
||||
void TestDecimals();
|
||||
|
||||
private:
|
||||
UnicodeString GetPatternAndSkipSyntax(const MessagePattern& pattern);
|
||||
|
|
Loading…
Add table
Reference in a new issue