ICU-11276 Adding NumberRangeFormatter (#87)

Formats number ranges, including currencies and measurement units.
This commit is contained in:
Shane 2018-09-16 02:07:37 -07:00 committed by Shane Carr
commit ce92011aff
No known key found for this signature in database
GPG Key ID: FCED3B24AAB18B5C
67 changed files with 6896 additions and 339 deletions

View File

@ -126,15 +126,21 @@ char *CharString::getAppendBuffer(int32_t minCapacity,
}
CharString &CharString::appendInvariantChars(const UnicodeString &s, UErrorCode &errorCode) {
return appendInvariantChars(s.getBuffer(), s.length(), errorCode);
}
CharString &CharString::appendInvariantChars(const UChar* uchars, int32_t ucharsLen, UErrorCode &errorCode) {
if(U_FAILURE(errorCode)) {
return *this;
}
if (!uprv_isInvariantUnicodeString(s)) {
if (!uprv_isInvariantUString(uchars, ucharsLen)) {
errorCode = U_INVARIANT_CONVERSION_ERROR;
return *this;
}
if(ensureCapacity(len+s.length()+1, 0, errorCode)) {
len+=s.extract(0, 0x7fffffff, buffer.getAlias()+len, buffer.getCapacity()-len, US_INV);
if(ensureCapacity(len+ucharsLen+1, 0, errorCode)) {
u_UCharsToChars(uchars, buffer.getAlias()+len, ucharsLen);
len += ucharsLen;
buffer[len] = 0;
}
return *this;
}

View File

@ -123,6 +123,7 @@ public:
UErrorCode &errorCode);
CharString &appendInvariantChars(const UnicodeString &s, UErrorCode &errorCode);
CharString &appendInvariantChars(const UChar* uchars, int32_t ucharsLen, UErrorCode& errorCode);
/**
* Appends a filename/path part, e.g., a directory name.

View File

@ -279,6 +279,10 @@ inline T *LocalMemory<T>::allocateInsteadAndCopy(int32_t newCapacity, int32_t le
*
* Unlike LocalMemory and LocalArray, this class never adopts
* (takes ownership of) another array.
*
* WARNING: MaybeStackArray only works with primitive (plain-old data) types.
* It does NOT know how to call a destructor! If you work with classes with
* destructors, consider LocalArray in localpointer.h.
*/
template<typename T, int32_t stackCapacity>
class MaybeStackArray {

View File

@ -53,22 +53,6 @@ uprv_isInvariantString(const char *s, int32_t length);
U_INTERNAL UBool U_EXPORT2
uprv_isInvariantUString(const UChar *s, int32_t length);
#ifdef __cplusplus
/**
* Check if a UnicodeString only contains invariant characters.
* See utypes.h for details.
*
* @param s Input string.
* @return TRUE if s contains only invariant characters.
*/
U_INTERNAL inline UBool U_EXPORT2
uprv_isInvariantUnicodeString(const icu::UnicodeString &s) {
return uprv_isInvariantUString(icu::toUCharPtr(s.getBuffer()), s.length());
}
#endif /* __cplusplus */
/**
* \def U_UPPER_ORDINAL
* Get the ordinal number of an uppercase invariant character

View File

@ -59,7 +59,7 @@
</taskdef>
</target>
<!-- target for generating ICU data -->
<target name="all" depends="locales, collation, rbnf, supplementalData, metadata, metaZones, windowsZones, likelySubtags, plurals, numberingSystems, translit, brkitr, keyTypeData, genderList, dayPeriods" />
<target name="all" depends="locales, collation, rbnf, supplementalData, metadata, metaZones, windowsZones, likelySubtags, plurals, pluralRanges, numberingSystems, translit, brkitr, keyTypeData, genderList, dayPeriods" />
<!-- parallel target -->
<target name="pall" depends="init">
<parallel threadsPerProcessor ="1">
@ -258,6 +258,18 @@
</run>
</cldr-build>
</target>
<target name="pluralRanges" depends="init,setup" description="builds pluralRanges.txt from pluralRanges.xml">
<cldr-build toolName="org.unicode.cldr.icu.NewLdml2IcuConverter" destFile="pluralRanges.txt" noArgs="true">
<!-- launch the tool and generate the data after reading the config file -->
<run>
<args>
<arg name="-s" value="${env.CLDR_DIR}/common/supplemental" />
<arg name="-d" value="${env.ICU4C_DIR}/source/data/misc"/>
<arg name="-t" value="pluralRanges"/>
</args>
</run>
</cldr-build>
</target>
<target name="numberingSystems" depends="init,setup" description="builds numberingSystems.txt from numberingSystems.xml">
<cldr-build toolName="org.unicode.cldr.icu.NewLdml2IcuConverter" destFile="numberingSystems.txt" noArgs="true">
<!-- launch the tool and generate the data after reading the config file -->
@ -414,6 +426,9 @@
<fileset id="plurals" dir="${env.ICU4C_DIR}/source/data/misc">
<include name="plurals.txt" />
</fileset>
<fileset id="pluralRanges" dir="${env.ICU4C_DIR}/source/data/misc">
<include name="pluralRanges.txt" />
</fileset>
<fileset id="numberingSystems" dir="${env.ICU4C_DIR}/source/data/misc">
<include name="numberingSystems.txt" />
</fileset>

View File

@ -28,4 +28,4 @@ MISC_SOURCE = \
zoneinfo64.txt supplementalData.txt likelySubtags.txt plurals.txt \
numberingSystems.txt icuver.txt icustd.txt metadata.txt metaZones.txt \
windowsZones.txt keyTypeData.txt timezoneTypes.txt currencyNumericCodes.txt \
genderList.txt dayPeriods.txt
genderList.txt dayPeriods.txt pluralRanges.txt

View File

@ -0,0 +1,981 @@
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
pluralRanges:table(nofallback){
locales{
af{"set05"}
ak{"set02"}
am{"set01"}
ar{"set18"}
as{"set01"}
az{"set04"}
be{"set15"}
bg{"set05"}
bn{"set01"}
bs{"set11"}
ca{"set05"}
cs{"set14"}
cy{"set17"}
da{"set06"}
de{"set04"}
el{"set04"}
en{"set05"}
es{"set05"}
et{"set05"}
eu{"set05"}
fa{"set02"}
fi{"set05"}
fil{"set06"}
fr{"set01"}
ga{"set16"}
gl{"set04"}
gsw{"set04"}
gu{"set01"}
he{"set13"}
hi{"set01"}
hr{"set11"}
hu{"set04"}
hy{"set01"}
id{"set00"}
io{"set05"}
is{"set06"}
it{"set04"}
ja{"set00"}
ka{"set03"}
kk{"set04"}
km{"set00"}
kn{"set01"}
ko{"set00"}
ky{"set04"}
lo{"set00"}
lt{"set15"}
lv{"set09"}
mk{"set08"}
ml{"set04"}
mn{"set04"}
mr{"set01"}
ms{"set00"}
my{"set00"}
nb{"set05"}
ne{"set04"}
nl{"set04"}
or{"set02"}
pa{"set06"}
pl{"set14"}
ps{"set01"}
pt{"set01"}
ro{"set10"}
ru{"set15"}
scn{"set04"}
sd{"set02"}
si{"set07"}
sk{"set14"}
sl{"set12"}
sq{"set04"}
sr{"set11"}
sv{"set05"}
sw{"set04"}
ta{"set04"}
te{"set04"}
th{"set00"}
tk{"set04"}
tr{"set04"}
ug{"set04"}
uk{"set15"}
ur{"set05"}
uz{"set04"}
vi{"set00"}
yue{"set00"}
zh{"set00"}
zu{"set01"}
}
rules{
set00{
{
"other",
"other",
"other",
}
}
set01{
{
"one",
"one",
"one",
}
{
"one",
"other",
"other",
}
{
"other",
"other",
"other",
}
}
set02{
{
"one",
"one",
"other",
}
{
"one",
"other",
"other",
}
{
"other",
"other",
"other",
}
}
set03{
{
"one",
"other",
"one",
}
{
"other",
"one",
"other",
}
{
"other",
"other",
"other",
}
}
set04{
{
"one",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"other",
"other",
}
}
set05{
{
"one",
"other",
"other",
}
{
"other",
"one",
"other",
}
{
"other",
"other",
"other",
}
}
set06{
{
"one",
"one",
"one",
}
{
"one",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"other",
"other",
}
}
set07{
{
"one",
"one",
"one",
}
{
"one",
"other",
"other",
}
{
"other",
"one",
"other",
}
{
"other",
"other",
"other",
}
}
set08{
{
"one",
"one",
"other",
}
{
"one",
"other",
"other",
}
{
"other",
"one",
"other",
}
{
"other",
"other",
"other",
}
}
set09{
{
"zero",
"zero",
"other",
}
{
"zero",
"one",
"one",
}
{
"zero",
"other",
"other",
}
{
"one",
"zero",
"other",
}
{
"one",
"one",
"one",
}
{
"one",
"other",
"other",
}
{
"other",
"zero",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"other",
"other",
}
}
set10{
{
"one",
"few",
"few",
}
{
"one",
"other",
"other",
}
{
"few",
"one",
"few",
}
{
"few",
"few",
"few",
}
{
"few",
"other",
"other",
}
{
"other",
"few",
"few",
}
{
"other",
"other",
"other",
}
}
set11{
{
"one",
"one",
"one",
}
{
"one",
"few",
"few",
}
{
"one",
"other",
"other",
}
{
"few",
"one",
"one",
}
{
"few",
"few",
"few",
}
{
"few",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"few",
"few",
}
{
"other",
"other",
"other",
}
}
set12{
{
"one",
"one",
"few",
}
{
"one",
"two",
"two",
}
{
"one",
"few",
"few",
}
{
"one",
"other",
"other",
}
{
"two",
"one",
"few",
}
{
"two",
"two",
"two",
}
{
"two",
"few",
"few",
}
{
"two",
"other",
"other",
}
{
"few",
"one",
"few",
}
{
"few",
"two",
"two",
}
{
"few",
"few",
"few",
}
{
"few",
"other",
"other",
}
{
"other",
"one",
"few",
}
{
"other",
"two",
"two",
}
{
"other",
"few",
"few",
}
{
"other",
"other",
"other",
}
}
set13{
{
"one",
"two",
"other",
}
{
"one",
"many",
"many",
}
{
"one",
"other",
"other",
}
{
"two",
"many",
"other",
}
{
"two",
"other",
"other",
}
{
"many",
"many",
"many",
}
{
"many",
"other",
"many",
}
{
"other",
"one",
"other",
}
{
"other",
"two",
"other",
}
{
"other",
"many",
"many",
}
{
"other",
"other",
"other",
}
}
set14{
{
"one",
"few",
"few",
}
{
"one",
"many",
"many",
}
{
"one",
"other",
"other",
}
{
"few",
"few",
"few",
}
{
"few",
"many",
"many",
}
{
"few",
"other",
"other",
}
{
"many",
"one",
"one",
}
{
"many",
"few",
"few",
}
{
"many",
"many",
"many",
}
{
"many",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"few",
"few",
}
{
"other",
"many",
"many",
}
{
"other",
"other",
"other",
}
}
set15{
{
"one",
"one",
"one",
}
{
"one",
"few",
"few",
}
{
"one",
"many",
"many",
}
{
"one",
"other",
"other",
}
{
"few",
"one",
"one",
}
{
"few",
"few",
"few",
}
{
"few",
"many",
"many",
}
{
"few",
"other",
"other",
}
{
"many",
"one",
"one",
}
{
"many",
"few",
"few",
}
{
"many",
"many",
"many",
}
{
"many",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"few",
"few",
}
{
"other",
"many",
"many",
}
{
"other",
"other",
"other",
}
}
set16{
{
"one",
"two",
"two",
}
{
"one",
"few",
"few",
}
{
"one",
"many",
"many",
}
{
"one",
"other",
"other",
}
{
"two",
"few",
"few",
}
{
"two",
"many",
"many",
}
{
"two",
"other",
"other",
}
{
"few",
"few",
"few",
}
{
"few",
"many",
"many",
}
{
"few",
"other",
"other",
}
{
"many",
"many",
"many",
}
{
"many",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"two",
"two",
}
{
"other",
"few",
"few",
}
{
"other",
"many",
"many",
}
{
"other",
"other",
"other",
}
}
set17{
{
"zero",
"one",
"one",
}
{
"zero",
"two",
"two",
}
{
"zero",
"few",
"few",
}
{
"zero",
"many",
"many",
}
{
"zero",
"other",
"other",
}
{
"one",
"two",
"two",
}
{
"one",
"few",
"few",
}
{
"one",
"many",
"many",
}
{
"one",
"other",
"other",
}
{
"two",
"few",
"few",
}
{
"two",
"many",
"many",
}
{
"two",
"other",
"other",
}
{
"few",
"many",
"many",
}
{
"few",
"other",
"other",
}
{
"many",
"other",
"other",
}
{
"other",
"one",
"one",
}
{
"other",
"two",
"two",
}
{
"other",
"few",
"few",
}
{
"other",
"many",
"many",
}
{
"other",
"other",
"other",
}
}
set18{
{
"zero",
"one",
"zero",
}
{
"zero",
"two",
"zero",
}
{
"zero",
"few",
"few",
}
{
"zero",
"many",
"many",
}
{
"zero",
"other",
"other",
}
{
"one",
"two",
"other",
}
{
"one",
"few",
"few",
}
{
"one",
"many",
"many",
}
{
"one",
"other",
"other",
}
{
"two",
"few",
"few",
}
{
"two",
"many",
"many",
}
{
"two",
"other",
"other",
}
{
"few",
"few",
"few",
}
{
"few",
"many",
"many",
}
{
"few",
"other",
"other",
}
{
"many",
"few",
"few",
}
{
"many",
"many",
"many",
}
{
"many",
"other",
"other",
}
{
"other",
"one",
"other",
}
{
"other",
"two",
"other",
}
{
"other",
"few",
"few",
}
{
"other",
"many",
"many",
}
{
"other",
"other",
"other",
}
}
}
}

View File

@ -111,9 +111,9 @@ double-conversion-fast-dtoa.o double-conversion-strtod.o \
numparse_stringsegment.o numparse_parsednumber.o numparse_impl.o \
numparse_symbols.o numparse_decimal.o numparse_scientific.o numparse_currency.o \
numparse_affixes.o numparse_compositions.o numparse_validators.o \
numrange_fluent.o numrange_impl.o \
erarules.o
## Header files to install
HEADERS = $(srcdir)/unicode/*.h

View File

@ -289,6 +289,8 @@
<ClCompile Include="numparse_affixes.cpp" />
<ClCompile Include="numparse_compositions.cpp" />
<ClCompile Include="numparse_validators.cpp" />
<ClCompile Include="numrange_fluent.cpp" />
<ClCompile Include="numrange_impl.cpp" />
<ClCompile Include="numfmt.cpp" />
<ClCompile Include="numsys.cpp" />
<ClCompile Include="olsontz.cpp" />
@ -552,6 +554,7 @@
<ClInclude Include="numparse_validators.h" />
<ClInclude Include="numparse_types.h" />
<ClInclude Include="numparse_utils.h" />
<ClInclude Include="numrange_impl.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="i18n.rc" />

View File

@ -624,6 +624,12 @@
<ClCompile Include="numparse_validators.cpp">
<Filter>formatting</Filter>
</ClCompile>
<ClCompile Include="numrange_fluent.cpp">
<Filter>formatting</Filter>
</ClCompile>
<ClCompile Include="numrange_impl.cpp">
<Filter>formatting</Filter>
</ClCompile>
<ClCompile Include="dayperiodrules.cpp">
<Filter>formatting</Filter>
</ClCompile>
@ -914,6 +920,9 @@
<ClInclude Include="numparse_utils.h">
<Filter>formatting</Filter>
</ClInclude>
<ClInclude Include="numrange_impl.h">
<Filter>formatting</Filter>
</ClInclude>
<ClInclude Include="olsontz.h">
<Filter>formatting</Filter>
</ClInclude>

View File

@ -396,6 +396,8 @@
<ClCompile Include="numparse_affixes.cpp" />
<ClCompile Include="numparse_compositions.cpp" />
<ClCompile Include="numparse_validators.cpp" />
<ClCompile Include="numrange_fluent.cpp" />
<ClCompile Include="numrange_impl.cpp" />
<ClCompile Include="numfmt.cpp" />
<ClCompile Include="numsys.cpp" />
<ClCompile Include="olsontz.cpp" />
@ -657,6 +659,7 @@
<ClInclude Include="numparse_validators.h" />
<ClInclude Include="numparse_types.h" />
<ClInclude Include="numparse_utils.h" />
<ClInclude Include="numrange_impl.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="i18n.rc" />

View File

@ -273,13 +273,13 @@ void CompactHandler::processQuantity(DecimalQuantity &quantity, MicroProps &micr
if (U_FAILURE(status)) { return; }
// Treat zero as if it had magnitude 0
int magnitude;
int32_t magnitude;
if (quantity.isZero()) {
magnitude = 0;
micros.rounder.apply(quantity, status);
} else {
// TODO: Revisit chooseMultiplierAndApply
int multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data, status);
int32_t multiplier = micros.rounder.chooseMultiplierAndApply(quantity, data, status);
magnitude = quantity.isZero() ? 0 : quantity.getMagnitude();
magnitude -= multiplier;
}

View File

@ -1154,8 +1154,31 @@ const char16_t* DecimalQuantity::checkHealth() const {
}
bool DecimalQuantity::operator==(const DecimalQuantity& other) const {
// FIXME: Make a faster implementation.
return toString() == other.toString();
bool basicEquals =
scale == other.scale
&& precision == other.precision
&& flags == other.flags
&& lOptPos == other.lOptPos
&& lReqPos == other.lReqPos
&& rReqPos == other.rReqPos
&& rOptPos == other.rOptPos
&& isApproximate == other.isApproximate;
if (!basicEquals) {
return false;
}
if (precision == 0) {
return true;
} else if (isApproximate) {
return origDouble == other.origDouble && origDelta == other.origDelta;
} else {
for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
if (getDigit(m) != other.getDigit(m)) {
return false;
}
}
return true;
}
}
UnicodeString DecimalQuantity::toString() const {

View File

@ -363,6 +363,7 @@ UnlocalizedNumberFormatter::UnlocalizedNumberFormatter(const NFS<UNF>& other)
// No additional fields to assign
}
// Make default copy constructor call the NumberFormatterSettings copy constructor.
UnlocalizedNumberFormatter::UnlocalizedNumberFormatter(UNF&& src) U_NOEXCEPT
: UNF(static_cast<NFS<UNF>&&>(src)) {}
@ -383,6 +384,7 @@ UnlocalizedNumberFormatter& UnlocalizedNumberFormatter::operator=(UNF&& src) U_N
return *this;
}
// Make default copy constructor call the NumberFormatterSettings copy constructor.
LocalizedNumberFormatter::LocalizedNumberFormatter(const LNF& other)
: LNF(static_cast<const NFS<LNF>&>(other)) {}
@ -657,9 +659,9 @@ LocalizedNumberFormatter::formatDecimalQuantity(const DecimalQuantity& dq, UErro
void LocalizedNumberFormatter::formatImpl(impl::UFormattedNumberData* results, UErrorCode& status) const {
if (computeCompiled(status)) {
fCompiled->apply(results->quantity, results->string, status);
fCompiled->format(results->quantity, results->string, status);
} else {
NumberFormatterImpl::applyStatic(fMacros, results->quantity, results->string, status);
NumberFormatterImpl::formatStatic(fMacros, results->quantity, results->string, status);
}
}
@ -706,7 +708,11 @@ bool LocalizedNumberFormatter::computeCompiled(UErrorCode& status) const {
if (currentCount == fMacros.threshold && fMacros.threshold > 0) {
// Build the data structure and then use it (slow to fast path).
const NumberFormatterImpl* compiled = NumberFormatterImpl::fromMacros(fMacros, status);
const NumberFormatterImpl* compiled = new NumberFormatterImpl(fMacros, status);
if (compiled == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return false;
}
U_ASSERT(fCompiled == nullptr);
const_cast<LocalizedNumberFormatter*>(this)->fCompiled = compiled;
umtx_storeRelease(*callCount, INT32_MIN);

View File

@ -67,14 +67,18 @@ getCurrencyFormatInfo(const Locale& locale, const char* isoCode, UErrorCode& sta
MicroPropsGenerator::~MicroPropsGenerator() = default;
NumberFormatterImpl* NumberFormatterImpl::fromMacros(const MacroProps& macros, UErrorCode& status) {
return new NumberFormatterImpl(macros, true, status);
NumberFormatterImpl::NumberFormatterImpl(const MacroProps& macros, UErrorCode& status)
: NumberFormatterImpl(macros, true, status) {
}
void NumberFormatterImpl::applyStatic(const MacroProps& macros, DecimalQuantity& inValue,
NumberStringBuilder& outString, UErrorCode& status) {
int32_t NumberFormatterImpl::formatStatic(const MacroProps& macros, DecimalQuantity& inValue,
NumberStringBuilder& outString, UErrorCode& status) {
NumberFormatterImpl impl(macros, false, status);
impl.applyUnsafe(inValue, outString, status);
MicroProps& micros = impl.preProcessUnsafe(inValue, status);
if (U_FAILURE(status)) { return 0; }
int32_t length = writeNumber(micros, inValue, outString, 0, status);
length += writeAffixes(micros, outString, 0, length, status);
return length;
}
int32_t NumberFormatterImpl::getPrefixSuffixStatic(const MacroProps& macros, int8_t signum,
@ -89,22 +93,40 @@ int32_t NumberFormatterImpl::getPrefixSuffixStatic(const MacroProps& macros, int
// The "unsafe" method simply re-uses fMicros, eliminating the extra copy operation.
// See MicroProps::processQuantity() for details.
void NumberFormatterImpl::apply(DecimalQuantity& inValue, NumberStringBuilder& outString,
int32_t NumberFormatterImpl::format(DecimalQuantity& inValue, NumberStringBuilder& outString,
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
MicroProps micros;
if (!fMicroPropsGenerator) { return; }
fMicroPropsGenerator->processQuantity(inValue, micros, status);
if (U_FAILURE(status)) { return; }
microsToString(micros, inValue, outString, status);
preProcess(inValue, micros, status);
if (U_FAILURE(status)) { return 0; }
int32_t length = writeNumber(micros, inValue, outString, 0, status);
length += writeAffixes(micros, outString, 0, length, status);
return length;
}
void NumberFormatterImpl::applyUnsafe(DecimalQuantity& inValue, NumberStringBuilder& outString,
UErrorCode& status) {
void NumberFormatterImpl::preProcess(DecimalQuantity& inValue, MicroProps& microsOut,
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
if (fMicroPropsGenerator == nullptr) {
status = U_INTERNAL_PROGRAM_ERROR;
return;
}
fMicroPropsGenerator->processQuantity(inValue, microsOut, status);
microsOut.rounder.apply(inValue, status);
microsOut.integerWidth.apply(inValue, status);
}
MicroProps& NumberFormatterImpl::preProcessUnsafe(DecimalQuantity& inValue, UErrorCode& status) {
if (U_FAILURE(status)) {
return fMicros; // must always return a value
}
if (fMicroPropsGenerator == nullptr) {
status = U_INTERNAL_PROGRAM_ERROR;
return fMicros; // must always return a value
}
fMicroPropsGenerator->processQuantity(inValue, fMicros, status);
if (U_FAILURE(status)) { return; }
microsToString(fMicros, inValue, outString, status);
fMicros.rounder.apply(inValue, status);
fMicros.integerWidth.apply(inValue, status);
return fMicros;
}
int32_t NumberFormatterImpl::getPrefixSuffix(int8_t signum, StandardPlural::Form plural,
@ -115,7 +137,7 @@ int32_t NumberFormatterImpl::getPrefixSuffix(int8_t signum, StandardPlural::Form
const Modifier* modifier = fImmutablePatternModifier->getModifier(signum, plural);
modifier->apply(outString, 0, 0, status);
if (U_FAILURE(status)) { return 0; }
return modifier->getPrefixLength(status);
return modifier->getPrefixLength();
}
int32_t NumberFormatterImpl::getPrefixSuffixUnsafe(int8_t signum, StandardPlural::Form plural,
@ -126,7 +148,7 @@ int32_t NumberFormatterImpl::getPrefixSuffixUnsafe(int8_t signum, StandardPlural
fPatternModifier->setNumberProperties(signum, plural);
fPatternModifier->apply(outString, 0, 0, status);
if (U_FAILURE(status)) { return 0; }
return fPatternModifier->getPrefixLength(status);
return fPatternModifier->getPrefixLength();
}
NumberFormatterImpl::NumberFormatterImpl(const MacroProps& macros, bool safe, UErrorCode& status) {
@ -344,25 +366,23 @@ NumberFormatterImpl::macrosToMicroGenerator(const MacroProps& macros, bool safe,
// Outer modifier (CLDR units and currency long names)
if (isCldrUnit) {
fLongNameHandler.adoptInstead(
new LongNameHandler(
LongNameHandler::forMeasureUnit(
macros.locale,
macros.unit,
macros.perUnit,
unitWidth,
resolvePluralRules(macros.rules, macros.locale, status),
chain,
status)));
LongNameHandler::forMeasureUnit(
macros.locale,
macros.unit,
macros.perUnit,
unitWidth,
resolvePluralRules(macros.rules, macros.locale, status),
chain,
status));
chain = fLongNameHandler.getAlias();
} else if (isCurrency && unitWidth == UNUM_UNIT_WIDTH_FULL_NAME) {
fLongNameHandler.adoptInstead(
new LongNameHandler(
LongNameHandler::forCurrencyLongNames(
macros.locale,
currency,
resolvePluralRules(macros.rules, macros.locale, status),
chain,
status)));
LongNameHandler::forCurrencyLongNames(
macros.locale,
currency,
resolvePluralRules(macros.rules, macros.locale, status),
chain,
status));
chain = fLongNameHandler.getAlias();
} else {
// No outer modifier required
@ -404,50 +424,46 @@ NumberFormatterImpl::resolvePluralRules(const PluralRules* rulesPtr, const Local
return fRules.getAlias();
}
int32_t NumberFormatterImpl::microsToString(const MicroProps& micros, DecimalQuantity& quantity,
NumberStringBuilder& string, UErrorCode& status) {
micros.rounder.apply(quantity, status);
micros.integerWidth.apply(quantity, status);
int32_t length = writeNumber(micros, quantity, string, status);
// NOTE: When range formatting is added, these modifiers can bubble up.
// For now, apply them all here at once.
int32_t NumberFormatterImpl::writeAffixes(const MicroProps& micros, NumberStringBuilder& string,
int32_t start, int32_t end, UErrorCode& status) {
// Always apply the inner modifier (which is "strong").
length += micros.modInner->apply(string, 0, length, status);
int32_t length = micros.modInner->apply(string, start, end, status);
if (micros.padding.isValid()) {
length += micros.padding
.padAndApply(*micros.modMiddle, *micros.modOuter, string, 0, length, status);
.padAndApply(*micros.modMiddle, *micros.modOuter, string, start, length + end, status);
} else {
length += micros.modMiddle->apply(string, 0, length, status);
length += micros.modOuter->apply(string, 0, length, status);
length += micros.modMiddle->apply(string, start, length + end, status);
length += micros.modOuter->apply(string, start, length + end, status);
}
return length;
}
int32_t NumberFormatterImpl::writeNumber(const MicroProps& micros, DecimalQuantity& quantity,
NumberStringBuilder& string, UErrorCode& status) {
NumberStringBuilder& string, int32_t index,
UErrorCode& status) {
int32_t length = 0;
if (quantity.isInfinite()) {
length += string.insert(
length,
length + index,
micros.symbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kInfinitySymbol),
UNUM_INTEGER_FIELD,
status);
} else if (quantity.isNaN()) {
length += string.insert(
length,
length + index,
micros.symbols->getSymbol(DecimalFormatSymbols::ENumberFormatSymbol::kNaNSymbol),
UNUM_INTEGER_FIELD,
status);
} else {
// Add the integer digits
length += writeIntegerDigits(micros, quantity, string, status);
length += writeIntegerDigits(micros, quantity, string, length + index, status);
// Add the decimal point
if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == UNUM_DECIMAL_SEPARATOR_ALWAYS) {
length += string.insert(
length,
length + index,
micros.useCurrency ? micros.symbols->getSymbol(
DecimalFormatSymbols::ENumberFormatSymbol::kMonetarySeparatorSymbol) : micros
.symbols
@ -458,21 +474,22 @@ int32_t NumberFormatterImpl::writeNumber(const MicroProps& micros, DecimalQuanti
}
// Add the fraction digits
length += writeFractionDigits(micros, quantity, string, status);
length += writeFractionDigits(micros, quantity, string, length + index, status);
}
return length;
}
int32_t NumberFormatterImpl::writeIntegerDigits(const MicroProps& micros, DecimalQuantity& quantity,
NumberStringBuilder& string, UErrorCode& status) {
NumberStringBuilder& string, int32_t index,
UErrorCode& status) {
int length = 0;
int integerCount = quantity.getUpperDisplayMagnitude() + 1;
for (int i = 0; i < integerCount; i++) {
// Add grouping separator
if (micros.grouping.groupAtPosition(i, quantity)) {
length += string.insert(
0,
index,
micros.useCurrency ? micros.symbols->getSymbol(
DecimalFormatSymbols::ENumberFormatSymbol::kMonetaryGroupingSeparatorSymbol)
: micros.symbols->getSymbol(
@ -484,20 +501,21 @@ int32_t NumberFormatterImpl::writeIntegerDigits(const MicroProps& micros, Decima
// Get and append the next digit value
int8_t nextDigit = quantity.getDigit(i);
length += utils::insertDigitFromSymbols(
string, 0, nextDigit, *micros.symbols, UNUM_INTEGER_FIELD, status);
string, index, nextDigit, *micros.symbols, UNUM_INTEGER_FIELD, status);
}
return length;
}
int32_t NumberFormatterImpl::writeFractionDigits(const MicroProps& micros, DecimalQuantity& quantity,
NumberStringBuilder& string, UErrorCode& status) {
NumberStringBuilder& string, int32_t index,
UErrorCode& status) {
int length = 0;
int fractionCount = -quantity.getLowerDisplayMagnitude();
for (int i = 0; i < fractionCount; i++) {
// Get and append the next digit value
int8_t nextDigit = quantity.getDigit(-i - 1);
length += utils::insertDigitFromSymbols(
string, string.length(), nextDigit, *micros.symbols, UNUM_FRACTION_FIELD, status);
string, length + index, nextDigit, *micros.symbols, UNUM_FRACTION_FIELD, status);
}
return length;
}

View File

@ -29,14 +29,14 @@ class NumberFormatterImpl : public UMemory {
* Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly.
* The caller owns the returned NumberFormatterImpl.
*/
static NumberFormatterImpl *fromMacros(const MacroProps &macros, UErrorCode &status);
NumberFormatterImpl(const MacroProps &macros, UErrorCode &status);
/**
* Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
*/
static void
applyStatic(const MacroProps &macros, DecimalQuantity &inValue, NumberStringBuilder &outString,
UErrorCode &status);
static int32_t
formatStatic(const MacroProps &macros, DecimalQuantity &inValue, NumberStringBuilder &outString,
UErrorCode &status);
/**
* Prints only the prefix and suffix; used for DecimalFormat getters.
@ -51,7 +51,12 @@ class NumberFormatterImpl : public UMemory {
/**
* Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
*/
void apply(DecimalQuantity& inValue, NumberStringBuilder& outString, UErrorCode& status) const;
int32_t format(DecimalQuantity& inValue, NumberStringBuilder& outString, UErrorCode& status) const;
/**
* Like format(), but saves the result into an output MicroProps without additional processing.
*/
void preProcess(DecimalQuantity& inValue, MicroProps& microsOut, UErrorCode& status) const;
/**
* Like getPrefixSuffixStatic() but uses the safe compiled object.
@ -59,6 +64,19 @@ class NumberFormatterImpl : public UMemory {
int32_t getPrefixSuffix(int8_t signum, StandardPlural::Form plural, NumberStringBuilder& outString,
UErrorCode& status) const;
/**
* Synthesizes the output string from a MicroProps and DecimalQuantity.
* This method formats only the main number, not affixes.
*/
static int32_t writeNumber(const MicroProps& micros, DecimalQuantity& quantity,
NumberStringBuilder& string, int32_t index, UErrorCode& status);
/**
* Adds the affixes. Intended to be called immediately after formatNumber.
*/
static int32_t writeAffixes(const MicroProps& micros, NumberStringBuilder& string, int32_t start,
int32_t end, UErrorCode& status);
private:
// Head of the MicroPropsGenerator linked list:
const MicroPropsGenerator *fMicroPropsGenerator = nullptr;
@ -85,7 +103,7 @@ class NumberFormatterImpl : public UMemory {
NumberFormatterImpl(const MacroProps &macros, bool safe, UErrorCode &status);
void applyUnsafe(DecimalQuantity &inValue, NumberStringBuilder &outString, UErrorCode &status);
MicroProps& preProcessUnsafe(DecimalQuantity &inValue, UErrorCode &status);
int32_t getPrefixSuffixUnsafe(int8_t signum, StandardPlural::Form plural,
NumberStringBuilder& outString, UErrorCode& status);
@ -113,31 +131,13 @@ class NumberFormatterImpl : public UMemory {
const MicroPropsGenerator *
macrosToMicroGenerator(const MacroProps &macros, bool safe, UErrorCode &status);
/**
* Synthesizes the output string from a MicroProps and DecimalQuantity.
*
* @param micros
* The MicroProps after the quantity has been consumed. Will not be mutated.
* @param quantity
* The DecimalQuantity to be rendered. May be mutated.
* @param string
* The output string. Will be mutated.
*/
static int32_t
microsToString(const MicroProps &micros, DecimalQuantity &quantity, NumberStringBuilder &string,
UErrorCode &status);
static int32_t
writeNumber(const MicroProps &micros, DecimalQuantity &quantity, NumberStringBuilder &string,
UErrorCode &status);
static int32_t
writeIntegerDigits(const MicroProps &micros, DecimalQuantity &quantity, NumberStringBuilder &string,
UErrorCode &status);
int32_t index, UErrorCode &status);
static int32_t
writeFractionDigits(const MicroProps &micros, DecimalQuantity &quantity, NumberStringBuilder &string,
UErrorCode &status);
int32_t index, UErrorCode &status);
};
} // namespace impl

View File

@ -39,7 +39,7 @@ static int32_t getIndex(const char* pluralKeyword, UErrorCode& status) {
static UnicodeString getWithPlural(
const UnicodeString* strings,
int32_t plural,
StandardPlural::Form plural,
UErrorCode& status) {
UnicodeString result = strings[plural];
if (result.isBogus()) {
@ -156,7 +156,7 @@ UnicodeString getPerUnitFormat(const Locale& locale, const UNumberUnitWidth &wid
} // namespace
LongNameHandler
LongNameHandler*
LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, const MeasureUnit &perUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status) {
@ -173,20 +173,28 @@ LongNameHandler::forMeasureUnit(const Locale &loc, const MeasureUnit &unitRef, c
}
}
LongNameHandler result(rules, parent);
auto* result = new LongNameHandler(rules, parent);
if (result == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return nullptr;
}
UnicodeString simpleFormats[ARRAY_LENGTH];
getMeasureData(loc, unit, width, simpleFormats, status);
if (U_FAILURE(status)) { return result; }
// TODO: What field to use for units?
simpleFormatsToModifiers(simpleFormats, UNUM_FIELD_COUNT, result.fModifiers, status);
result->simpleFormatsToModifiers(simpleFormats, UNUM_FIELD_COUNT, status);
return result;
}
LongNameHandler
LongNameHandler*
LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status) {
LongNameHandler result(rules, parent);
auto* result = new LongNameHandler(rules, parent);
if (result == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return nullptr;
}
UnicodeString primaryData[ARRAY_LENGTH];
getMeasureData(loc, unit, width, primaryData, status);
if (U_FAILURE(status)) { return result; }
@ -213,46 +221,52 @@ LongNameHandler::forCompoundUnit(const Locale &loc, const MeasureUnit &unit, con
if (U_FAILURE(status)) { return result; }
}
// TODO: What field to use for units?
multiSimpleFormatsToModifiers(primaryData, perUnitFormat, UNUM_FIELD_COUNT, result.fModifiers, status);
result->multiSimpleFormatsToModifiers(primaryData, perUnitFormat, UNUM_FIELD_COUNT, status);
return result;
}
LongNameHandler LongNameHandler::forCurrencyLongNames(const Locale &loc, const CurrencyUnit &currency,
LongNameHandler* LongNameHandler::forCurrencyLongNames(const Locale &loc, const CurrencyUnit &currency,
const PluralRules *rules,
const MicroPropsGenerator *parent,
UErrorCode &status) {
LongNameHandler result(rules, parent);
auto* result = new LongNameHandler(rules, parent);
if (result == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return nullptr;
}
UnicodeString simpleFormats[ARRAY_LENGTH];
getCurrencyLongNameData(loc, currency, simpleFormats, status);
if (U_FAILURE(status)) { return result; }
simpleFormatsToModifiers(simpleFormats, UNUM_CURRENCY_FIELD, result.fModifiers, status);
if (U_FAILURE(status)) { return nullptr; }
result->simpleFormatsToModifiers(simpleFormats, UNUM_CURRENCY_FIELD, status);
return result;
}
void LongNameHandler::simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field,
SimpleModifier *output, UErrorCode &status) {
UErrorCode &status) {
for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) {
UnicodeString simpleFormat = getWithPlural(simpleFormats, i, status);
StandardPlural::Form plural = static_cast<StandardPlural::Form>(i);
UnicodeString simpleFormat = getWithPlural(simpleFormats, plural, status);
if (U_FAILURE(status)) { return; }
SimpleFormatter compiledFormatter(simpleFormat, 0, 1, status);
if (U_FAILURE(status)) { return; }
output[i] = SimpleModifier(compiledFormatter, field, false);
fModifiers[i] = SimpleModifier(compiledFormatter, field, false, {this, 0, plural});
}
}
void LongNameHandler::multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat,
Field field, SimpleModifier *output, UErrorCode &status) {
Field field, UErrorCode &status) {
SimpleFormatter trailCompiled(trailFormat, 1, 1, status);
if (U_FAILURE(status)) { return; }
for (int32_t i = 0; i < StandardPlural::Form::COUNT; i++) {
UnicodeString leadFormat = getWithPlural(leadFormats, i, status);
StandardPlural::Form plural = static_cast<StandardPlural::Form>(i);
UnicodeString leadFormat = getWithPlural(leadFormats, plural, status);
if (U_FAILURE(status)) { return; }
UnicodeString compoundFormat;
trailCompiled.format(leadFormat, compoundFormat, status);
if (U_FAILURE(status)) { return; }
SimpleFormatter compoundCompiled(compoundFormat, 0, 1, status);
if (U_FAILURE(status)) { return; }
output[i] = SimpleModifier(compoundCompiled, field, false);
fModifiers[i] = SimpleModifier(compoundCompiled, field, false, {this, 0, plural});
}
}
@ -265,4 +279,8 @@ void LongNameHandler::processQuantity(DecimalQuantity &quantity, MicroProps &mic
micros.modOuter = &fModifiers[utils::getStandardPlural(rules, copy)];
}
const Modifier* LongNameHandler::getModifier(int8_t /*signum*/, StandardPlural::Form plural) const {
return &fModifiers[plural];
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -14,13 +14,13 @@
U_NAMESPACE_BEGIN namespace number {
namespace impl {
class LongNameHandler : public MicroPropsGenerator, public UMemory {
class LongNameHandler : public MicroPropsGenerator, public ModifierStore, public UMemory {
public:
static LongNameHandler
static LongNameHandler*
forCurrencyLongNames(const Locale &loc, const CurrencyUnit &currency, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status);
static LongNameHandler
static LongNameHandler*
forMeasureUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status);
@ -28,6 +28,8 @@ class LongNameHandler : public MicroPropsGenerator, public UMemory {
void
processQuantity(DecimalQuantity &quantity, MicroProps &micros, UErrorCode &status) const U_OVERRIDE;
const Modifier* getModifier(int8_t signum, StandardPlural::Form plural) const U_OVERRIDE;
private:
SimpleModifier fModifiers[StandardPlural::Form::COUNT];
const PluralRules *rules;
@ -36,15 +38,14 @@ class LongNameHandler : public MicroPropsGenerator, public UMemory {
LongNameHandler(const PluralRules *rules, const MicroPropsGenerator *parent)
: rules(rules), parent(parent) {}
static LongNameHandler
static LongNameHandler*
forCompoundUnit(const Locale &loc, const MeasureUnit &unit, const MeasureUnit &perUnit,
const UNumberUnitWidth &width, const PluralRules *rules,
const MicroPropsGenerator *parent, UErrorCode &status);
static void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field,
SimpleModifier *output, UErrorCode &status);
static void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat,
Field field, SimpleModifier *output, UErrorCode &status);
void simpleFormatsToModifiers(const UnicodeString *simpleFormats, Field field, UErrorCode &status);
void multiSimpleFormatsToModifiers(const UnicodeString *leadFormats, UnicodeString trailFormat,
Field field, UErrorCode &status);
};
} // namespace impl

View File

@ -53,6 +53,21 @@ void U_CALLCONV initDefaultCurrencySpacing(UErrorCode &status) {
Modifier::~Modifier() = default;
Modifier::Parameters::Parameters()
: obj(nullptr) {}
Modifier::Parameters::Parameters(
const ModifierStore* _obj, int8_t _signum, StandardPlural::Form _plural)
: obj(_obj), signum(_signum), plural(_plural) {}
ModifierStore::~ModifierStore() = default;
AdoptingModifierStore::~AdoptingModifierStore() {
for (const Modifier *mod : mods) {
delete mod;
}
}
int32_t ConstantAffixModifier::apply(NumberStringBuilder &output, int leftIndex, int rightIndex,
UErrorCode &status) const {
@ -62,13 +77,11 @@ int32_t ConstantAffixModifier::apply(NumberStringBuilder &output, int leftIndex,
return length;
}
int32_t ConstantAffixModifier::getPrefixLength(UErrorCode &status) const {
(void)status;
int32_t ConstantAffixModifier::getPrefixLength() const {
return fPrefix.length();
}
int32_t ConstantAffixModifier::getCodePointCount(UErrorCode &status) const {
(void)status;
int32_t ConstantAffixModifier::getCodePointCount() const {
return fPrefix.countChar32() + fSuffix.countChar32();
}
@ -76,8 +89,38 @@ bool ConstantAffixModifier::isStrong() const {
return fStrong;
}
bool ConstantAffixModifier::containsField(UNumberFormatFields field) const {
(void)field;
// This method is not currently used.
U_ASSERT(false);
return false;
}
void ConstantAffixModifier::getParameters(Parameters& output) const {
(void)output;
// This method is not currently used.
U_ASSERT(false);
}
bool ConstantAffixModifier::semanticallyEquivalent(const Modifier& other) const {
auto* _other = dynamic_cast<const ConstantAffixModifier*>(&other);
if (_other == nullptr) {
return false;
}
return fPrefix == _other->fPrefix
&& fSuffix == _other->fSuffix
&& fField == _other->fField
&& fStrong == _other->fStrong;
}
SimpleModifier::SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong)
: fCompiledPattern(simpleFormatter.compiledPattern), fField(field), fStrong(strong) {
: SimpleModifier(simpleFormatter, field, strong, {}) {}
SimpleModifier::SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong,
const Modifier::Parameters parameters)
: fCompiledPattern(simpleFormatter.compiledPattern), fField(field), fStrong(strong),
fParameters(parameters) {
int32_t argLimit = SimpleFormatter::getArgumentLimit(
fCompiledPattern.getBuffer(), fCompiledPattern.length());
if (argLimit == 0) {
@ -90,15 +133,19 @@ SimpleModifier::SimpleModifier(const SimpleFormatter &simpleFormatter, Field fie
} else {
U_ASSERT(argLimit == 1);
if (fCompiledPattern.charAt(1) != 0) {
// Found prefix
fPrefixLength = fCompiledPattern.charAt(1) - ARG_NUM_LIMIT;
fSuffixOffset = 3 + fPrefixLength;
} else {
// No prefix
fPrefixLength = 0;
fSuffixOffset = 2;
}
if (3 + fPrefixLength < fCompiledPattern.length()) {
// Found suffix
fSuffixLength = fCompiledPattern.charAt(fSuffixOffset) - ARG_NUM_LIMIT;
} else {
// No suffix
fSuffixLength = 0;
}
}
@ -113,13 +160,11 @@ int32_t SimpleModifier::apply(NumberStringBuilder &output, int leftIndex, int ri
return formatAsPrefixSuffix(output, leftIndex, rightIndex, fField, status);
}
int32_t SimpleModifier::getPrefixLength(UErrorCode &status) const {
(void)status;
int32_t SimpleModifier::getPrefixLength() const {
return fPrefixLength;
}
int32_t SimpleModifier::getCodePointCount(UErrorCode &status) const {
(void)status;
int32_t SimpleModifier::getCodePointCount() const {
int32_t count = 0;
if (fPrefixLength > 0) {
count += fCompiledPattern.countChar32(2, fPrefixLength);
@ -134,10 +179,35 @@ bool SimpleModifier::isStrong() const {
return fStrong;
}
bool SimpleModifier::containsField(UNumberFormatFields field) const {
(void)field;
// This method is not currently used.
U_ASSERT(false);
return false;
}
void SimpleModifier::getParameters(Parameters& output) const {
output = fParameters;
}
bool SimpleModifier::semanticallyEquivalent(const Modifier& other) const {
auto* _other = dynamic_cast<const SimpleModifier*>(&other);
if (_other == nullptr) {
return false;
}
if (fParameters.obj != nullptr) {
return fParameters.obj == _other->fParameters.obj;
}
return fCompiledPattern == _other->fCompiledPattern
&& fField == _other->fField
&& fStrong == _other->fStrong;
}
int32_t
SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startIndex, int32_t endIndex,
Field field, UErrorCode &status) const {
if (fSuffixOffset == -1) {
if (fSuffixOffset == -1 && fPrefixLength + fSuffixLength > 0) {
// There is no argument for the inner number; overwrite the entire segment with our string.
return result.splice(startIndex, endIndex, fCompiledPattern, 2, 2 + fPrefixLength, field, status);
} else {
@ -157,6 +227,65 @@ SimpleModifier::formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startI
}
}
int32_t
SimpleModifier::formatTwoArgPattern(const SimpleFormatter& compiled, NumberStringBuilder& result,
int32_t index, int32_t* outPrefixLength, int32_t* outSuffixLength,
Field field, UErrorCode& status) {
const UnicodeString& compiledPattern = compiled.compiledPattern;
int32_t argLimit = SimpleFormatter::getArgumentLimit(
compiledPattern.getBuffer(), compiledPattern.length());
if (argLimit != 2) {
status = U_INTERNAL_PROGRAM_ERROR;
return 0;
}
int32_t offset = 1; // offset into compiledPattern
int32_t length = 0; // chars added to result
int32_t prefixLength = compiledPattern.charAt(offset);
offset++;
if (prefixLength < ARG_NUM_LIMIT) {
// No prefix
prefixLength = 0;
} else {
prefixLength -= ARG_NUM_LIMIT;
result.insert(index + length, compiledPattern, offset, offset + prefixLength, field, status);
offset += prefixLength;
length += prefixLength;
offset++;
}
int32_t infixLength = compiledPattern.charAt(offset);
offset++;
if (infixLength < ARG_NUM_LIMIT) {
// No infix
infixLength = 0;
} else {
infixLength -= ARG_NUM_LIMIT;
result.insert(index + length, compiledPattern, offset, offset + infixLength, field, status);
offset += infixLength;
length += infixLength;
offset++;
}
int32_t suffixLength;
if (offset == compiledPattern.length()) {
// No suffix
suffixLength = 0;
} else {
suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
offset++;
result.insert(index + length, compiledPattern, offset, offset + suffixLength, field, status);
length += suffixLength;
}
*outPrefixLength = prefixLength;
*outSuffixLength = suffixLength;
return length;
}
int32_t ConstantMultiFieldModifier::apply(NumberStringBuilder &output, int leftIndex, int rightIndex,
UErrorCode &status) const {
int32_t length = output.insert(leftIndex, fPrefix, status);
@ -171,13 +300,11 @@ int32_t ConstantMultiFieldModifier::apply(NumberStringBuilder &output, int leftI
return length;
}
int32_t ConstantMultiFieldModifier::getPrefixLength(UErrorCode &status) const {
(void)status;
int32_t ConstantMultiFieldModifier::getPrefixLength() const {
return fPrefix.length();
}
int32_t ConstantMultiFieldModifier::getCodePointCount(UErrorCode &status) const {
(void)status;
int32_t ConstantMultiFieldModifier::getCodePointCount() const {
return fPrefix.codePointCount() + fSuffix.codePointCount();
}
@ -185,6 +312,29 @@ bool ConstantMultiFieldModifier::isStrong() const {
return fStrong;
}
bool ConstantMultiFieldModifier::containsField(UNumberFormatFields field) const {
return fPrefix.containsField(field) || fSuffix.containsField(field);
}
void ConstantMultiFieldModifier::getParameters(Parameters& output) const {
output = fParameters;
}
bool ConstantMultiFieldModifier::semanticallyEquivalent(const Modifier& other) const {
auto* _other = dynamic_cast<const ConstantMultiFieldModifier*>(&other);
if (_other == nullptr) {
return false;
}
if (fParameters.obj != nullptr) {
return fParameters.obj == _other->fParameters.obj;
}
return fPrefix.contentEquals(_other->fPrefix)
&& fSuffix.contentEquals(_other->fSuffix)
&& fOverwrite == _other->fOverwrite
&& fStrong == _other->fStrong;
}
CurrencySpacingEnabledModifier::CurrencySpacingEnabledModifier(const NumberStringBuilder &prefix,
const NumberStringBuilder &suffix,
bool overwrite,

View File

@ -31,12 +31,18 @@ class U_I18N_API ConstantAffixModifier : public Modifier, public UObject {
int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex,
UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength() const U_OVERRIDE;
int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE;
int32_t getCodePointCount() const U_OVERRIDE;
bool isStrong() const U_OVERRIDE;
bool containsField(UNumberFormatFields field) const U_OVERRIDE;
void getParameters(Parameters& output) const U_OVERRIDE;
bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE;
private:
UnicodeString fPrefix;
UnicodeString fSuffix;
@ -52,21 +58,30 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory {
public:
SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong);
SimpleModifier(const SimpleFormatter &simpleFormatter, Field field, bool strong,
const Modifier::Parameters parameters);
// Default constructor for LongNameHandler.h
SimpleModifier();
int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex,
UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength() const U_OVERRIDE;
int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE;
int32_t getCodePointCount() const U_OVERRIDE;
bool isStrong() const U_OVERRIDE;
bool containsField(UNumberFormatFields field) const U_OVERRIDE;
void getParameters(Parameters& output) const U_OVERRIDE;
bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE;
/**
* TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because
* DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not depend on it.
* NumberStringBuilder is an internal class and SimpleFormatterImpl feels like it should not depend on it.
*
* <p>
* Formats a value that is already stored inside the StringBuilder <code>result</code> between the indices
@ -85,16 +100,33 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory {
* @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
*/
int32_t
formatAsPrefixSuffix(NumberStringBuilder &result, int32_t startIndex, int32_t endIndex, Field field,
UErrorCode &status) const;
formatAsPrefixSuffix(NumberStringBuilder& result, int32_t startIndex, int32_t endIndex, Field field,
UErrorCode& status) const;
/**
* TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code.
* I put it here so that the SimpleFormatter uses in NumberStringBuilder are near each other.
*
* <p>
* Applies the compiled two-argument pattern to the NumberStringBuilder.
*
* <p>
* This method is optimized for the case where the prefix and suffix are often empty, such as
* in the range pattern like "{0}-{1}".
*/
static int32_t
formatTwoArgPattern(const SimpleFormatter& compiled, NumberStringBuilder& result,
int32_t index, int32_t* outPrefixLength, int32_t* outSuffixLength,
Field field, UErrorCode& status);
private:
UnicodeString fCompiledPattern;
Field fField;
bool fStrong;
int32_t fPrefixLength;
int32_t fSuffixOffset;
int32_t fSuffixLength;
bool fStrong = false;
int32_t fPrefixLength = 0;
int32_t fSuffixOffset = -1;
int32_t fSuffixLength = 0;
Modifier::Parameters fParameters;
};
/**
@ -103,6 +135,18 @@ class U_I18N_API SimpleModifier : public Modifier, public UMemory {
*/
class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory {
public:
ConstantMultiFieldModifier(
const NumberStringBuilder &prefix,
const NumberStringBuilder &suffix,
bool overwrite,
bool strong,
const Modifier::Parameters parameters)
: fPrefix(prefix),
fSuffix(suffix),
fOverwrite(overwrite),
fStrong(strong),
fParameters(parameters) {}
ConstantMultiFieldModifier(
const NumberStringBuilder &prefix,
const NumberStringBuilder &suffix,
@ -116,12 +160,18 @@ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory {
int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex,
UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength() const U_OVERRIDE;
int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE;
int32_t getCodePointCount() const U_OVERRIDE;
bool isStrong() const U_OVERRIDE;
bool containsField(UNumberFormatFields field) const U_OVERRIDE;
void getParameters(Parameters& output) const U_OVERRIDE;
bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE;
protected:
// NOTE: In Java, these are stored as array pointers. In C++, the NumberStringBuilder is stored by
// value and is treated internally as immutable.
@ -129,6 +179,7 @@ class U_I18N_API ConstantMultiFieldModifier : public Modifier, public UMemory {
NumberStringBuilder fSuffix;
bool fOverwrite;
bool fStrong;
Modifier::Parameters fParameters;
};
/** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */
@ -192,13 +243,11 @@ class U_I18N_API EmptyModifier : public Modifier, public UMemory {
return 0;
}
int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE {
(void)status;
int32_t getPrefixLength() const U_OVERRIDE {
return 0;
}
int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE {
(void)status;
int32_t getCodePointCount() const U_OVERRIDE {
return 0;
}
@ -206,55 +255,75 @@ class U_I18N_API EmptyModifier : public Modifier, public UMemory {
return fStrong;
}
bool containsField(UNumberFormatFields field) const U_OVERRIDE {
(void)field;
return false;
}
void getParameters(Parameters& output) const U_OVERRIDE {
output.obj = nullptr;
}
bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE {
return other.getCodePointCount() == 0;
}
private:
bool fStrong;
};
/**
* A ParameterizedModifier by itself is NOT a Modifier. Rather, it wraps a data structure containing two or more
* Modifiers and returns the modifier appropriate for the current situation.
* This implementation of ModifierStore adopts Modifer pointers.
*/
class U_I18N_API ParameterizedModifier : public UMemory {
class U_I18N_API AdoptingModifierStore : public ModifierStore, public UMemory {
public:
// NOTE: mods is zero-initialized (to nullptr)
ParameterizedModifier() : mods() {
}
virtual ~AdoptingModifierStore();
static constexpr StandardPlural::Form DEFAULT_STANDARD_PLURAL = StandardPlural::OTHER;
AdoptingModifierStore() = default;
// No copying!
ParameterizedModifier(const ParameterizedModifier &other) = delete;
AdoptingModifierStore(const AdoptingModifierStore &other) = delete;
~ParameterizedModifier() {
for (const Modifier *mod : mods) {
delete mod;
}
}
void adoptPositiveNegativeModifiers(
const Modifier *positive, const Modifier *zero, const Modifier *negative) {
mods[2] = positive;
mods[1] = zero;
mods[0] = negative;
}
/** The modifier is ADOPTED. */
void adoptSignPluralModifier(int8_t signum, StandardPlural::Form plural, const Modifier *mod) {
/**
* Sets the Modifier with the specified signum and plural form.
*/
void adoptModifier(int8_t signum, StandardPlural::Form plural, const Modifier *mod) {
U_ASSERT(mods[getModIndex(signum, plural)] == nullptr);
mods[getModIndex(signum, plural)] = mod;
}
/** Returns a reference to the modifier; no ownership change. */
const Modifier *getModifier(int8_t signum) const {
return mods[signum + 1];
/**
* Sets the Modifier with the specified signum.
* The modifier will apply to all plural forms.
*/
void adoptModifierWithoutPlural(int8_t signum, const Modifier *mod) {
U_ASSERT(mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)] == nullptr);
mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)] = mod;
}
/** Returns a reference to the modifier; no ownership change. */
const Modifier *getModifier(int8_t signum, StandardPlural::Form plural) const {
return mods[getModIndex(signum, plural)];
const Modifier *getModifier(int8_t signum, StandardPlural::Form plural) const U_OVERRIDE {
const Modifier* modifier = mods[getModIndex(signum, plural)];
if (modifier == nullptr && plural != DEFAULT_STANDARD_PLURAL) {
modifier = mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)];
}
return modifier;
}
/** Returns a reference to the modifier; no ownership change. */
const Modifier *getModifierWithoutPlural(int8_t signum) const {
return mods[getModIndex(signum, DEFAULT_STANDARD_PLURAL)];
}
private:
const Modifier *mods[3 * StandardPlural::COUNT];
// NOTE: mods is zero-initialized (to nullptr)
const Modifier *mods[3 * StandardPlural::COUNT] = {};
inline static int32_t getModIndex(int8_t signum, StandardPlural::Form plural) {
U_ASSERT(signum >= -1 && signum <= 1);
U_ASSERT(plural >= 0 && plural < StandardPlural::COUNT);
return static_cast<int32_t>(plural) * 3 + (signum + 1);
}
};

View File

@ -62,7 +62,7 @@ Padder Padder::forProperties(const DecimalFormatProperties& properties) {
int32_t Padder::padAndApply(const Modifier &mod1, const Modifier &mod2,
NumberStringBuilder &string, int32_t leftIndex, int32_t rightIndex,
UErrorCode &status) const {
int32_t modLength = mod1.getCodePointCount(status) + mod2.getCodePointCount(status);
int32_t modLength = mod1.getCodePointCount() + mod2.getCodePointCount();
int32_t requiredPadding = fWidth - modLength - string.codePointCount();
U_ASSERT(leftIndex == 0 &&
rightIndex == string.length()); // fix the previous line to remove this assertion

View File

@ -69,7 +69,7 @@ MutablePatternModifier::createImmutableAndChain(const MicroPropsGenerator* paren
StandardPlural::Form::MANY,
StandardPlural::Form::OTHER};
auto pm = new ParameterizedModifier();
auto pm = new AdoptingModifierStore();
if (pm == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return nullptr;
@ -79,11 +79,11 @@ MutablePatternModifier::createImmutableAndChain(const MicroPropsGenerator* paren
// Slower path when we require the plural keyword.
for (StandardPlural::Form plural : STANDARD_PLURAL_VALUES) {
setNumberProperties(1, plural);
pm->adoptSignPluralModifier(1, plural, createConstantModifier(status));
pm->adoptModifier(1, plural, createConstantModifier(status));
setNumberProperties(0, plural);
pm->adoptSignPluralModifier(0, plural, createConstantModifier(status));
pm->adoptModifier(0, plural, createConstantModifier(status));
setNumberProperties(-1, plural);
pm->adoptSignPluralModifier(-1, plural, createConstantModifier(status));
pm->adoptModifier(-1, plural, createConstantModifier(status));
}
if (U_FAILURE(status)) {
delete pm;
@ -93,12 +93,11 @@ MutablePatternModifier::createImmutableAndChain(const MicroPropsGenerator* paren
} else {
// Faster path when plural keyword is not needed.
setNumberProperties(1, StandardPlural::Form::COUNT);
Modifier* positive = createConstantModifier(status);
pm->adoptModifierWithoutPlural(1, createConstantModifier(status));
setNumberProperties(0, StandardPlural::Form::COUNT);
Modifier* zero = createConstantModifier(status);
pm->adoptModifierWithoutPlural(0, createConstantModifier(status));
setNumberProperties(-1, StandardPlural::Form::COUNT);
Modifier* negative = createConstantModifier(status);
pm->adoptPositiveNegativeModifiers(positive, zero, negative);
pm->adoptModifierWithoutPlural(-1, createConstantModifier(status));
if (U_FAILURE(status)) {
delete pm;
return nullptr;
@ -120,7 +119,7 @@ ConstantMultiFieldModifier* MutablePatternModifier::createConstantModifier(UErro
}
}
ImmutablePatternModifier::ImmutablePatternModifier(ParameterizedModifier* pm, const PluralRules* rules,
ImmutablePatternModifier::ImmutablePatternModifier(AdoptingModifierStore* pm, const PluralRules* rules,
const MicroPropsGenerator* parent)
: pm(pm), rules(rules), parent(parent) {}
@ -132,7 +131,7 @@ void ImmutablePatternModifier::processQuantity(DecimalQuantity& quantity, MicroP
void ImmutablePatternModifier::applyToMicros(MicroProps& micros, DecimalQuantity& quantity) const {
if (rules == nullptr) {
micros.modMiddle = pm->getModifier(quantity.signum());
micros.modMiddle = pm->getModifierWithoutPlural(quantity.signum());
} else {
// TODO: Fix this. Avoid the copy.
DecimalQuantity copy(quantity);
@ -144,7 +143,7 @@ void ImmutablePatternModifier::applyToMicros(MicroProps& micros, DecimalQuantity
const Modifier* ImmutablePatternModifier::getModifier(int8_t signum, StandardPlural::Form plural) const {
if (rules == nullptr) {
return pm->getModifier(signum);
return pm->getModifierWithoutPlural(signum);
} else {
return pm->getModifier(signum, plural);
}
@ -204,23 +203,25 @@ int32_t MutablePatternModifier::apply(NumberStringBuilder& output, int32_t leftI
return prefixLen + overwriteLen + suffixLen;
}
int32_t MutablePatternModifier::getPrefixLength(UErrorCode& status) const {
int32_t MutablePatternModifier::getPrefixLength() const {
// The unsafe code path performs self-mutation, so we need a const_cast.
// This method needs to be const because it overrides a const method in the parent class.
auto nonConstThis = const_cast<MutablePatternModifier*>(this);
// Enter and exit CharSequence Mode to get the length.
UErrorCode status = U_ZERO_ERROR; // status fails only with an iilegal argument exception
nonConstThis->prepareAffix(true);
int result = AffixUtils::unescapedCodePointCount(currentAffix, *this, status); // prefix length
return result;
}
int32_t MutablePatternModifier::getCodePointCount(UErrorCode& status) const {
int32_t MutablePatternModifier::getCodePointCount() const {
// The unsafe code path performs self-mutation, so we need a const_cast.
// This method needs to be const because it overrides a const method in the parent class.
auto nonConstThis = const_cast<MutablePatternModifier*>(this);
// Render the affixes to get the length
UErrorCode status = U_ZERO_ERROR; // status fails only with an iilegal argument exception
nonConstThis->prepareAffix(true);
int result = AffixUtils::unescapedCodePointCount(currentAffix, *this, status); // prefix length
nonConstThis->prepareAffix(false);
@ -232,6 +233,26 @@ bool MutablePatternModifier::isStrong() const {
return fStrong;
}
bool MutablePatternModifier::containsField(UNumberFormatFields field) const {
(void)field;
// This method is not currently used.
U_ASSERT(false);
return false;
}
void MutablePatternModifier::getParameters(Parameters& output) const {
(void)output;
// This method is not currently used.
U_ASSERT(false);
}
bool MutablePatternModifier::semanticallyEquivalent(const Modifier& other) const {
(void)other;
// This method is not currently used.
U_ASSERT(false);
return false;
}
int32_t MutablePatternModifier::insertPrefix(NumberStringBuilder& sb, int position, UErrorCode& status) {
prepareAffix(true);
int length = AffixUtils::unescape(currentAffix, sb, position, *this, status);

View File

@ -18,13 +18,13 @@
U_NAMESPACE_BEGIN
// Export an explicit template instantiation of the LocalPointer that is used as a
// data member of ParameterizedModifier.
// data member of AdoptingModifierStore.
// (When building DLLs for Windows this is required.)
#if U_PF_WINDOWS <= U_PLATFORM && U_PLATFORM <= U_PF_CYGWIN
// Ignore warning 4661 as LocalPointerBase does not use operator== or operator!=
#pragma warning(suppress: 4661)
template class U_I18N_API LocalPointerBase<number::impl::ParameterizedModifier>;
template class U_I18N_API LocalPointer<number::impl::ParameterizedModifier>;
template class U_I18N_API LocalPointerBase<number::impl::AdoptingModifierStore>;
template class U_I18N_API LocalPointer<number::impl::AdoptingModifierStore>;
#endif
namespace number {
@ -45,10 +45,10 @@ class U_I18N_API ImmutablePatternModifier : public MicroPropsGenerator, public U
const Modifier* getModifier(int8_t signum, StandardPlural::Form plural) const;
private:
ImmutablePatternModifier(ParameterizedModifier* pm, const PluralRules* rules,
ImmutablePatternModifier(AdoptingModifierStore* pm, const PluralRules* rules,
const MicroPropsGenerator* parent);
const LocalPointer<ParameterizedModifier> pm;
const LocalPointer<AdoptingModifierStore> pm;
const PluralRules* rules;
const MicroPropsGenerator* parent;
@ -178,12 +178,18 @@ class U_I18N_API MutablePatternModifier
int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex,
UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength() const U_OVERRIDE;
int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE;
int32_t getCodePointCount() const U_OVERRIDE;
bool isStrong() const U_OVERRIDE;
bool containsField(UNumberFormatFields field) const U_OVERRIDE;
void getParameters(Parameters& output) const U_OVERRIDE;
bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE;
/**
* Returns the string that substitutes a given symbol type in a pattern.
*/

View File

@ -76,17 +76,16 @@ int32_t ScientificModifier::apply(NumberStringBuilder &output, int32_t /*leftInd
return i - rightIndex;
}
int32_t ScientificModifier::getPrefixLength(UErrorCode &status) const {
(void)status;
int32_t ScientificModifier::getPrefixLength() const {
// TODO: Localized exponent separator location.
return 0;
}
int32_t ScientificModifier::getCodePointCount(UErrorCode &status) const {
(void)status;
// This method is not used for strong modifiers.
U_ASSERT(false);
return 0;
int32_t ScientificModifier::getCodePointCount() const {
// NOTE: This method is only called one place, NumberRangeFormatterImpl.
// The call site only cares about != 0 and != 1.
// Return a very large value so that if this method is used elsewhere, we should notice.
return 999;
}
bool ScientificModifier::isStrong() const {
@ -94,6 +93,27 @@ bool ScientificModifier::isStrong() const {
return true;
}
bool ScientificModifier::containsField(UNumberFormatFields field) const {
(void)field;
// This method is not used for inner modifiers.
U_ASSERT(false);
return false;
}
void ScientificModifier::getParameters(Parameters& output) const {
// Not part of any plural sets
output.obj = nullptr;
}
bool ScientificModifier::semanticallyEquivalent(const Modifier& other) const {
auto* _other = dynamic_cast<const ScientificModifier*>(&other);
if (_other == nullptr) {
return false;
}
// TODO: Check for locale symbols and settings as well? Could be less efficient.
return fExponent == _other->fExponent;
}
// Note: Visual Studio does not compile this function without full name space. Why?
icu::number::impl::ScientificHandler::ScientificHandler(const Notation *notation, const DecimalFormatSymbols *symbols,
const MicroPropsGenerator *parent) :

View File

@ -24,12 +24,18 @@ class U_I18N_API ScientificModifier : public UMemory, public Modifier {
int32_t apply(NumberStringBuilder &output, int32_t leftIndex, int32_t rightIndex,
UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength(UErrorCode &status) const U_OVERRIDE;
int32_t getPrefixLength() const U_OVERRIDE;
int32_t getCodePointCount(UErrorCode &status) const U_OVERRIDE;
int32_t getCodePointCount() const U_OVERRIDE;
bool isStrong() const U_OVERRIDE;
bool containsField(UNumberFormatFields field) const U_OVERRIDE;
void getParameters(Parameters& output) const U_OVERRIDE;
bool semanticallyEquivalent(const Modifier& other) const U_OVERRIDE;
private:
int32_t fExponent;
const ScientificHandler *fHandler;

View File

@ -241,6 +241,9 @@ NumberStringBuilder::insert(int32_t index, const NumberStringBuilder &other, UEr
}
int32_t NumberStringBuilder::prepareForInsert(int32_t index, int32_t count, UErrorCode &status) {
U_ASSERT(index >= 0);
U_ASSERT(index <= fLength);
U_ASSERT(count >= 0);
if (index == 0 && fZero - count >= 0) {
// Append to start
fZero -= count;
@ -485,4 +488,13 @@ void NumberStringBuilder::getAllFieldPositions(FieldPositionIteratorHandler& fpi
}
}
bool NumberStringBuilder::containsField(Field field) const {
for (int32_t i = 0; i < fLength; i++) {
if (field == fieldAt(i)) {
return true;
}
}
return false;
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -106,6 +106,8 @@ class U_I18N_API NumberStringBuilder : public UMemory {
void getAllFieldPositions(FieldPositionIteratorHandler& fpih, UErrorCode& status) const;
bool containsField(Field field) const;
private:
bool fUsingHeap = false;
ValueOrHeapArray<char16_t> fChars;

View File

@ -16,6 +16,7 @@
#include "uassert.h"
#include "unicode/platform.h"
#include "unicode/uniset.h"
#include "standardplural.h"
U_NAMESPACE_BEGIN namespace number {
namespace impl {
@ -45,6 +46,7 @@ class Modifier;
class MutablePatternModifier;
class DecimalQuantity;
class NumberStringBuilder;
class ModifierStore;
struct MicroProps;
@ -127,12 +129,13 @@ class U_I18N_API AffixPatternProvider {
virtual bool hasBody() const = 0;
};
/**
* A Modifier is an object that can be passed through the formatting pipeline until it is finally applied to the string
* builder. A Modifier usually contains a prefix and a suffix that are applied, but it could contain something else,
* like a {@link com.ibm.icu.text.SimpleFormatter} pattern.
*
* A Modifier is usually immutable, except in cases such as {@link MurkyModifier}, which are mutable for performance
* A Modifier is usually immutable, except in cases such as {@link MutablePatternModifier}, which are mutable for performance
* reasons.
*
* Exported as U_I18N_API because it is a base class for other exported types
@ -162,12 +165,12 @@ class U_I18N_API Modifier {
*
* @return The number of characters (UTF-16 code units) in the prefix.
*/
virtual int32_t getPrefixLength(UErrorCode& status) const = 0;
virtual int32_t getPrefixLength() const = 0;
/**
* Returns the number of code points in the modifier, prefix plus suffix.
*/
virtual int32_t getCodePointCount(UErrorCode& status) const = 0;
virtual int32_t getCodePointCount() const = 0;
/**
* Whether this modifier is strong. If a modifier is strong, it should always be applied immediately and not allowed
@ -177,8 +180,57 @@ class U_I18N_API Modifier {
* @return Whether the modifier is strong.
*/
virtual bool isStrong() const = 0;
/**
* Whether the modifier contains at least one occurrence of the given field.
*/
virtual bool containsField(UNumberFormatFields field) const = 0;
/**
* A fill-in for getParameters(). obj will always be set; if non-null, the other
* two fields are also safe to read.
*/
struct Parameters {
const ModifierStore* obj = nullptr;
int8_t signum;
StandardPlural::Form plural;
Parameters();
Parameters(const ModifierStore* _obj, int8_t _signum, StandardPlural::Form _plural);
};
/**
* Gets a set of "parameters" for this Modifier.
*
* TODO: Make this return a `const Parameters*` more like Java?
*/
virtual void getParameters(Parameters& output) const = 0;
/**
* Returns whether this Modifier is *semantically equivalent* to the other Modifier;
* in many cases, this is the same as equal, but parameters should be ignored.
*/
virtual bool semanticallyEquivalent(const Modifier& other) const = 0;
};
/**
* This is *not* a modifier; rather, it is an object that can return modifiers
* based on given parameters.
*
* Exported as U_I18N_API because it is a base class for other exported types.
*/
class U_I18N_API ModifierStore {
public:
virtual ~ModifierStore();
/**
* Returns a Modifier with the given parameters (best-effort).
*/
virtual const Modifier* getModifier(int8_t signum, StandardPlural::Form plural) const = 0;
};
/**
* This interface is used when all number formatting settings, including the locale, are known, except for the quantity
* itself. The {@link #processQuantity} method performs the final step in the number processing pipeline: it uses the

View File

@ -0,0 +1,430 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
#include "unicode/utypes.h"
#if !UCONFIG_NO_FORMATTING
// Allow implicit conversion from char16_t* to UnicodeString for this file:
// Helpful in toString methods and elsewhere.
#define UNISTR_FROM_STRING_EXPLICIT
#include "numrange_impl.h"
#include "util.h"
#include "number_utypes.h"
using namespace icu;
using namespace icu::number;
using namespace icu::number::impl;
// This function needs to be declared in this namespace so it can be friended.
// NOTE: In Java, this logic is handled in the resolve() function.
void icu::number::impl::touchRangeLocales(RangeMacroProps& macros) {
macros.formatter1.fMacros.locale = macros.locale;
macros.formatter2.fMacros.locale = macros.locale;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(const UnlocalizedNumberFormatter& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter1 = formatter;
copy.fMacros.singleFormatter = true;
touchRangeLocales(copy.fMacros);
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(const UnlocalizedNumberFormatter& formatter) && {
Derived move(std::move(*this));
move.fMacros.formatter1 = formatter;
move.fMacros.singleFormatter = true;
touchRangeLocales(move.fMacros);
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(UnlocalizedNumberFormatter&& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter1 = std::move(formatter);
copy.fMacros.singleFormatter = true;
touchRangeLocales(copy.fMacros);
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterBoth(UnlocalizedNumberFormatter&& formatter) && {
Derived move(std::move(*this));
move.fMacros.formatter1 = std::move(formatter);
move.fMacros.singleFormatter = true;
touchRangeLocales(move.fMacros);
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(const UnlocalizedNumberFormatter& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter1 = formatter;
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(const UnlocalizedNumberFormatter& formatter) && {
Derived move(std::move(*this));
move.fMacros.formatter1 = formatter;
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(UnlocalizedNumberFormatter&& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter1 = std::move(formatter);
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterFirst(UnlocalizedNumberFormatter&& formatter) && {
Derived move(std::move(*this));
move.fMacros.formatter1 = std::move(formatter);
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(const UnlocalizedNumberFormatter& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter2 = formatter;
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(const UnlocalizedNumberFormatter& formatter) && {
Derived move(std::move(*this));
move.fMacros.formatter2 = formatter;
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(UnlocalizedNumberFormatter&& formatter) const& {
Derived copy(*this);
copy.fMacros.formatter2 = std::move(formatter);
copy.fMacros.singleFormatter = false;
touchRangeLocales(copy.fMacros);
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::numberFormatterSecond(UnlocalizedNumberFormatter&& formatter) && {
Derived move(std::move(*this));
move.fMacros.formatter2 = std::move(formatter);
move.fMacros.singleFormatter = false;
touchRangeLocales(move.fMacros);
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::collapse(UNumberRangeCollapse collapse) const& {
Derived copy(*this);
copy.fMacros.collapse = collapse;
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::collapse(UNumberRangeCollapse collapse) && {
Derived move(std::move(*this));
move.fMacros.collapse = collapse;
return move;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::identityFallback(UNumberRangeIdentityFallback identityFallback) const& {
Derived copy(*this);
copy.fMacros.identityFallback = identityFallback;
return copy;
}
template<typename Derived>
Derived NumberRangeFormatterSettings<Derived>::identityFallback(UNumberRangeIdentityFallback identityFallback) && {
Derived move(std::move(*this));
move.fMacros.identityFallback = identityFallback;
return move;
}
// Declare all classes that implement NumberRangeFormatterSettings
// See https://stackoverflow.com/a/495056/1407170
template
class icu::number::NumberRangeFormatterSettings<icu::number::UnlocalizedNumberRangeFormatter>;
template
class icu::number::NumberRangeFormatterSettings<icu::number::LocalizedNumberRangeFormatter>;
UnlocalizedNumberRangeFormatter NumberRangeFormatter::with() {
UnlocalizedNumberRangeFormatter result;
return result;
}
LocalizedNumberRangeFormatter NumberRangeFormatter::withLocale(const Locale& locale) {
return with().locale(locale);
}
template<typename T> using NFS = NumberRangeFormatterSettings<T>;
using LNF = LocalizedNumberRangeFormatter;
using UNF = UnlocalizedNumberRangeFormatter;
UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(const UNF& other)
: UNF(static_cast<const NFS<UNF>&>(other)) {}
UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(const NFS<UNF>& other)
: NFS<UNF>(other) {
// No additional fields to assign
}
// Make default copy constructor call the NumberRangeFormatterSettings copy constructor.
UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(UNF&& src) U_NOEXCEPT
: UNF(static_cast<NFS<UNF>&&>(src)) {}
UnlocalizedNumberRangeFormatter::UnlocalizedNumberRangeFormatter(NFS<UNF>&& src) U_NOEXCEPT
: NFS<UNF>(std::move(src)) {
// No additional fields to assign
}
UnlocalizedNumberRangeFormatter& UnlocalizedNumberRangeFormatter::operator=(const UNF& other) {
NFS<UNF>::operator=(static_cast<const NFS<UNF>&>(other));
// No additional fields to assign
return *this;
}
UnlocalizedNumberRangeFormatter& UnlocalizedNumberRangeFormatter::operator=(UNF&& src) U_NOEXCEPT {
NFS<UNF>::operator=(static_cast<NFS<UNF>&&>(src));
// No additional fields to assign
return *this;
}
// Make default copy constructor call the NumberRangeFormatterSettings copy constructor.
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const LNF& other)
: LNF(static_cast<const NFS<LNF>&>(other)) {}
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const NFS<LNF>& other)
: NFS<LNF>(other) {
// No additional fields to assign
}
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(LocalizedNumberRangeFormatter&& src) U_NOEXCEPT
: LNF(static_cast<NFS<LNF>&&>(src)) {}
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(NFS<LNF>&& src) U_NOEXCEPT
: NFS<LNF>(std::move(src)) {
// No additional fields to assign
}
LocalizedNumberRangeFormatter& LocalizedNumberRangeFormatter::operator=(const LNF& other) {
NFS<LNF>::operator=(static_cast<const NFS<LNF>&>(other));
// No additional fields to assign
return *this;
}
LocalizedNumberRangeFormatter& LocalizedNumberRangeFormatter::operator=(LNF&& src) U_NOEXCEPT {
NFS<LNF>::operator=(static_cast<NFS<LNF>&&>(src));
// No additional fields to assign
return *this;
}
LocalizedNumberRangeFormatter::~LocalizedNumberRangeFormatter() {
delete fImpl;
}
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(const RangeMacroProps& macros, const Locale& locale) {
fMacros = macros;
fMacros.locale = locale;
touchRangeLocales(fMacros);
}
LocalizedNumberRangeFormatter::LocalizedNumberRangeFormatter(RangeMacroProps&& macros, const Locale& locale) {
fMacros = std::move(macros);
fMacros.locale = locale;
touchRangeLocales(fMacros);
}
LocalizedNumberRangeFormatter UnlocalizedNumberRangeFormatter::locale(const Locale& locale) const& {
return LocalizedNumberRangeFormatter(fMacros, locale);
}
LocalizedNumberRangeFormatter UnlocalizedNumberRangeFormatter::locale(const Locale& locale)&& {
return LocalizedNumberRangeFormatter(std::move(fMacros), locale);
}
FormattedNumberRange LocalizedNumberRangeFormatter::formatFormattableRange(
const Formattable& first, const Formattable& second, UErrorCode& status) const {
if (U_FAILURE(status)) {
return FormattedNumberRange(U_ILLEGAL_ARGUMENT_ERROR);
}
auto results = new UFormattedNumberRangeData();
if (results == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return FormattedNumberRange(status);
}
first.populateDecimalQuantity(results->quantity1, status);
if (U_FAILURE(status)) {
return FormattedNumberRange(status);
}
second.populateDecimalQuantity(results->quantity2, status);
if (U_FAILURE(status)) {
return FormattedNumberRange(status);
}
formatImpl(*results, first == second, status);
// Do not save the results object if we encountered a failure.
if (U_SUCCESS(status)) {
return FormattedNumberRange(results);
} else {
delete results;
return FormattedNumberRange(status);
}
}
void LocalizedNumberRangeFormatter::formatImpl(
UFormattedNumberRangeData& results, bool equalBeforeRounding, UErrorCode& status) const {
if (fImpl == nullptr) {
// TODO: Fix this once the atomic is ready!
auto* nonConstThis = const_cast<LocalizedNumberRangeFormatter*>(this);
nonConstThis->fImpl = new NumberRangeFormatterImpl(fMacros, status);
if (U_FAILURE(status)) {
return;
}
if (fImpl == nullptr) {
status = U_MEMORY_ALLOCATION_ERROR;
return;
}
}
fImpl->format(results, equalBeforeRounding, status);
}
FormattedNumberRange::FormattedNumberRange(FormattedNumberRange&& src) U_NOEXCEPT
: fResults(src.fResults), fErrorCode(src.fErrorCode) {
// Disown src.fResults to prevent double-deletion
src.fResults = nullptr;
src.fErrorCode = U_INVALID_STATE_ERROR;
}
FormattedNumberRange& FormattedNumberRange::operator=(FormattedNumberRange&& src) U_NOEXCEPT {
delete fResults;
fResults = src.fResults;
fErrorCode = src.fErrorCode;
// Disown src.fResults to prevent double-deletion
src.fResults = nullptr;
src.fErrorCode = U_INVALID_STATE_ERROR;
return *this;
}
UnicodeString FormattedNumberRange::toString(UErrorCode& status) const {
if (U_FAILURE(status)) {
return ICU_Utility::makeBogusString();
}
if (fResults == nullptr) {
status = fErrorCode;
return ICU_Utility::makeBogusString();
}
return fResults->string.toUnicodeString();
}
Appendable& FormattedNumberRange::appendTo(Appendable& appendable, UErrorCode& status) const {
if (U_FAILURE(status)) {
return appendable;
}
if (fResults == nullptr) {
status = fErrorCode;
return appendable;
}
appendable.appendString(fResults->string.chars(), fResults->string.length());
return appendable;
}
UBool FormattedNumberRange::nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const {
if (U_FAILURE(status)) {
return FALSE;
}
if (fResults == nullptr) {
status = fErrorCode;
return FALSE;
}
// NOTE: MSVC sometimes complains when implicitly converting between bool and UBool
return fResults->string.nextFieldPosition(fieldPosition, status) ? TRUE : FALSE;
}
void FormattedNumberRange::getAllFieldPositions(FieldPositionIterator& iterator, UErrorCode& status) const {
FieldPositionIteratorHandler fpih(&iterator, status);
getAllFieldPositionsImpl(fpih, status);
}
void FormattedNumberRange::getAllFieldPositionsImpl(
FieldPositionIteratorHandler& fpih, UErrorCode& status) const {
if (U_FAILURE(status)) {
return;
}
if (fResults == nullptr) {
status = fErrorCode;
return;
}
fResults->string.getAllFieldPositions(fpih, status);
}
UnicodeString FormattedNumberRange::getFirstDecimal(UErrorCode& status) const {
if (U_FAILURE(status)) {
return ICU_Utility::makeBogusString();
}
if (fResults == nullptr) {
status = fErrorCode;
return ICU_Utility::makeBogusString();
}
return fResults->quantity1.toScientificString();
}
UnicodeString FormattedNumberRange::getSecondDecimal(UErrorCode& status) const {
if (U_FAILURE(status)) {
return ICU_Utility::makeBogusString();
}
if (fResults == nullptr) {
status = fErrorCode;
return ICU_Utility::makeBogusString();
}
return fResults->quantity2.toScientificString();
}
UNumberRangeIdentityResult FormattedNumberRange::getIdentityResult(UErrorCode& status) const {
if (U_FAILURE(status)) {
return UNUM_IDENTITY_RESULT_NOT_EQUAL;
}
if (fResults == nullptr) {
status = fErrorCode;
return UNUM_IDENTITY_RESULT_NOT_EQUAL;
}
return fResults->identityResult;
}
FormattedNumberRange::~FormattedNumberRange() {
delete fResults;
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -0,0 +1,483 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
#include "unicode/utypes.h"
#if !UCONFIG_NO_FORMATTING
// Allow implicit conversion from char16_t* to UnicodeString for this file:
// Helpful in toString methods and elsewhere.
#define UNISTR_FROM_STRING_EXPLICIT
#include "unicode/numberrangeformatter.h"
#include "numrange_impl.h"
#include "patternprops.h"
#include "uresimp.h"
#include "util.h"
using namespace icu;
using namespace icu::number;
using namespace icu::number::impl;
namespace {
// Helper function for 2-dimensional switch statement
constexpr int8_t identity2d(UNumberRangeIdentityFallback a, UNumberRangeIdentityResult b) {
return static_cast<int8_t>(a) | (static_cast<int8_t>(b) << 4);
}
struct NumberRangeData {
SimpleFormatter rangePattern;
SimpleFormatter approximatelyPattern;
};
class NumberRangeDataSink : public ResourceSink {
public:
NumberRangeDataSink(NumberRangeData& data) : fData(data) {}
void put(const char* key, ResourceValue& value, UBool /*noFallback*/, UErrorCode& status) U_OVERRIDE {
ResourceTable miscTable = value.getTable(status);
if (U_FAILURE(status)) { return; }
for (int i = 0; miscTable.getKeyAndValue(i, key, value); i++) {
if (uprv_strcmp(key, "range") == 0) {
if (fData.rangePattern.getArgumentLimit() != 0) {
continue; // have already seen this pattern
}
fData.rangePattern = {value.getUnicodeString(status), status};
} else if (uprv_strcmp(key, "approximately") == 0) {
if (fData.approximatelyPattern.getArgumentLimit() != 0) {
continue; // have already seen this pattern
}
fData.approximatelyPattern = {value.getUnicodeString(status), status};
}
}
}
private:
NumberRangeData& fData;
};
void getNumberRangeData(const char* localeName, const char* nsName, NumberRangeData& data, UErrorCode& status) {
if (U_FAILURE(status)) { return; }
LocalUResourceBundlePointer rb(ures_open(NULL, localeName, &status));
if (U_FAILURE(status)) { return; }
NumberRangeDataSink sink(data);
CharString dataPath;
dataPath.append("NumberElements/", -1, status);
dataPath.append(nsName, -1, status);
dataPath.append("/miscPatterns", -1, status);
ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status);
if (U_FAILURE(status)) { return; }
// TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that?
if (data.rangePattern.getArgumentLimit() == 0) {
// No data!
data.rangePattern = {u"{0}{1}", status};
}
if (data.approximatelyPattern.getArgumentLimit() == 0) {
// No data!
data.approximatelyPattern = {u"~{0}", status};
}
}
class PluralRangesDataSink : public ResourceSink {
public:
PluralRangesDataSink(StandardPluralRanges& output) : fOutput(output) {}
void put(const char* /*key*/, ResourceValue& value, UBool /*noFallback*/, UErrorCode& status) U_OVERRIDE {
ResourceArray entriesArray = value.getArray(status);
if (U_FAILURE(status)) { return; }
fOutput.setCapacity(entriesArray.getSize());
for (int i = 0; entriesArray.getValue(i, value); i++) {
ResourceArray pluralFormsArray = value.getArray(status);
if (U_FAILURE(status)) { return; }
pluralFormsArray.getValue(0, value);
StandardPlural::Form first = StandardPlural::fromString(value.getUnicodeString(status), status);
if (U_FAILURE(status)) { return; }
pluralFormsArray.getValue(1, value);
StandardPlural::Form second = StandardPlural::fromString(value.getUnicodeString(status), status);
if (U_FAILURE(status)) { return; }
pluralFormsArray.getValue(2, value);
StandardPlural::Form result = StandardPlural::fromString(value.getUnicodeString(status), status);
if (U_FAILURE(status)) { return; }
fOutput.addPluralRange(first, second, result);
}
}
private:
StandardPluralRanges& fOutput;
};
void getPluralRangesData(const Locale& locale, StandardPluralRanges& output, UErrorCode& status) {
if (U_FAILURE(status)) { return; }
LocalUResourceBundlePointer rb(ures_openDirect(nullptr, "pluralRanges", &status));
if (U_FAILURE(status)) { return; }
CharString dataPath;
dataPath.append("locales/", -1, status);
dataPath.append(locale.getLanguage(), -1, status);
if (U_FAILURE(status)) { return; }
int32_t setLen;
// Not all languages are covered: fail gracefully
UErrorCode internalStatus = U_ZERO_ERROR;
const UChar* set = ures_getStringByKeyWithFallback(rb.getAlias(), dataPath.data(), &setLen, &internalStatus);
if (U_FAILURE(internalStatus)) { return; }
dataPath.clear();
dataPath.append("rules/", -1, status);
dataPath.appendInvariantChars(set, setLen, status);
if (U_FAILURE(status)) { return; }
PluralRangesDataSink sink(output);
ures_getAllItemsWithFallback(rb.getAlias(), dataPath.data(), sink, status);
if (U_FAILURE(status)) { return; }
}
} // namespace
void StandardPluralRanges::initialize(const Locale& locale, UErrorCode& status) {
getPluralRangesData(locale, *this, status);
}
void StandardPluralRanges::addPluralRange(
StandardPlural::Form first,
StandardPlural::Form second,
StandardPlural::Form result) {
U_ASSERT(fTriplesLen < fTriples.getCapacity());
fTriples[fTriplesLen] = {first, second, result};
fTriplesLen++;
}
void StandardPluralRanges::setCapacity(int32_t length) {
if (length > fTriples.getCapacity()) {
fTriples.resize(length, 0);
}
}
StandardPlural::Form
StandardPluralRanges::resolve(StandardPlural::Form first, StandardPlural::Form second) const {
for (int32_t i=0; i<fTriplesLen; i++) {
const auto& triple = fTriples[i];
if (triple.first == first && triple.second == second) {
return triple.result;
}
}
// Default fallback
return StandardPlural::OTHER;
}
NumberRangeFormatterImpl::NumberRangeFormatterImpl(const RangeMacroProps& macros, UErrorCode& status)
: formatterImpl1(macros.formatter1.fMacros, status),
formatterImpl2(macros.formatter2.fMacros, status),
fSameFormatters(macros.singleFormatter),
fCollapse(macros.collapse),
fIdentityFallback(macros.identityFallback) {
// TODO: As of this writing (ICU 63), there is no locale that has different number miscPatterns
// based on numbering system. Therefore, data is loaded only from latn. If this changes,
// this part of the code should be updated to load from the local numbering system.
// The numbering system could come from the one specified in the NumberFormatter passed to
// numberFormatterBoth() or similar.
// See ICU-20144
NumberRangeData data;
getNumberRangeData(macros.locale.getName(), "latn", data, status);
if (U_FAILURE(status)) { return; }
fRangeFormatter = data.rangePattern;
fApproximatelyModifier = {data.approximatelyPattern, UNUM_FIELD_COUNT, false};
// TODO: Get locale from PluralRules instead?
fPluralRanges.initialize(macros.locale, status);
if (U_FAILURE(status)) { return; }
}
void NumberRangeFormatterImpl::format(UFormattedNumberRangeData& data, bool equalBeforeRounding, UErrorCode& status) const {
if (U_FAILURE(status)) {
return;
}
MicroProps micros1;
MicroProps micros2;
formatterImpl1.preProcess(data.quantity1, micros1, status);
if (fSameFormatters) {
formatterImpl1.preProcess(data.quantity2, micros2, status);
} else {
formatterImpl2.preProcess(data.quantity2, micros2, status);
}
if (U_FAILURE(status)) {
return;
}
// If any of the affixes are different, an identity is not possible
// and we must use formatRange().
// TODO: Write this as MicroProps operator==() ?
// TODO: Avoid the redundancy of these equality operations with the
// ones in formatRange?
if (!micros1.modInner->semanticallyEquivalent(*micros2.modInner)
|| !micros1.modMiddle->semanticallyEquivalent(*micros2.modMiddle)
|| !micros1.modOuter->semanticallyEquivalent(*micros2.modOuter)) {
formatRange(data, micros1, micros2, status);
data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL;
return;
}
// Check for identity
if (equalBeforeRounding) {
data.identityResult = UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING;
} else if (data.quantity1 == data.quantity2) {
data.identityResult = UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING;
} else {
data.identityResult = UNUM_IDENTITY_RESULT_NOT_EQUAL;
}
switch (identity2d(fIdentityFallback, data.identityResult)) {
case identity2d(UNUM_IDENTITY_FALLBACK_RANGE,
UNUM_IDENTITY_RESULT_NOT_EQUAL):
case identity2d(UNUM_IDENTITY_FALLBACK_RANGE,
UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING):
case identity2d(UNUM_IDENTITY_FALLBACK_RANGE,
UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING):
case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY,
UNUM_IDENTITY_RESULT_NOT_EQUAL):
case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE,
UNUM_IDENTITY_RESULT_NOT_EQUAL):
case identity2d(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE,
UNUM_IDENTITY_RESULT_NOT_EQUAL):
formatRange(data, micros1, micros2, status);
break;
case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY,
UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING):
case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY,
UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING):
case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE,
UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING):
formatApproximately(data, micros1, micros2, status);
break;
case identity2d(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE,
UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING):
case identity2d(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE,
UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING):
case identity2d(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE,
UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING):
formatSingleValue(data, micros1, micros2, status);
break;
default:
U_ASSERT(false);
break;
}
}
void NumberRangeFormatterImpl::formatSingleValue(UFormattedNumberRangeData& data,
MicroProps& micros1, MicroProps& micros2,
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
if (fSameFormatters) {
int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status);
NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status);
} else {
formatRange(data, micros1, micros2, status);
}
}
void NumberRangeFormatterImpl::formatApproximately (UFormattedNumberRangeData& data,
MicroProps& micros1, MicroProps& micros2,
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
if (fSameFormatters) {
int32_t length = NumberFormatterImpl::writeNumber(micros1, data.quantity1, data.string, 0, status);
length += NumberFormatterImpl::writeAffixes(micros1, data.string, 0, length, status);
fApproximatelyModifier.apply(data.string, 0, length, status);
} else {
formatRange(data, micros1, micros2, status);
}
}
void NumberRangeFormatterImpl::formatRange(UFormattedNumberRangeData& data,
MicroProps& micros1, MicroProps& micros2,
UErrorCode& status) const {
if (U_FAILURE(status)) { return; }
// modInner is always notation (scientific); collapsable in ALL.
// modOuter is always units; collapsable in ALL, AUTO, and UNIT.
// modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT.
// Never collapse an outer mod but not an inner mod.
bool collapseOuter, collapseMiddle, collapseInner;
switch (fCollapse) {
case UNUM_RANGE_COLLAPSE_ALL:
case UNUM_RANGE_COLLAPSE_AUTO:
case UNUM_RANGE_COLLAPSE_UNIT:
{
// OUTER MODIFIER
collapseOuter = micros1.modOuter->semanticallyEquivalent(*micros2.modOuter);
if (!collapseOuter) {
// Never collapse inner mods if outer mods are not collapsable
collapseMiddle = false;
collapseInner = false;
break;
}
// MIDDLE MODIFIER
collapseMiddle = micros1.modMiddle->semanticallyEquivalent(*micros2.modMiddle);
if (!collapseMiddle) {
// Never collapse inner mods if outer mods are not collapsable
collapseInner = false;
break;
}
// MIDDLE MODIFIER HEURISTICS
// (could disable collapsing of the middle modifier)
// The modifiers are equal by this point, so we can look at just one of them.
const Modifier* mm = micros1.modMiddle;
if (fCollapse == UNUM_RANGE_COLLAPSE_UNIT) {
// Only collapse if the modifier is a unit.
// TODO: Make a better way to check for a unit?
// TODO: Handle case where the modifier has both notation and unit (compact currency)?
if (!mm->containsField(UNUM_CURRENCY_FIELD) && !mm->containsField(UNUM_PERCENT_FIELD)) {
collapseMiddle = false;
}
} else if (fCollapse == UNUM_RANGE_COLLAPSE_AUTO) {
// Heuristic as of ICU 63: collapse only if the modifier is more than one code point.
if (mm->getCodePointCount() <= 1) {
collapseMiddle = false;
}
}
if (!collapseMiddle || fCollapse != UNUM_RANGE_COLLAPSE_ALL) {
collapseInner = false;
break;
}
// INNER MODIFIER
collapseInner = micros1.modInner->semanticallyEquivalent(*micros2.modInner);
// All done checking for collapsability.
break;
}
default:
collapseOuter = false;
collapseMiddle = false;
collapseInner = false;
break;
}
NumberStringBuilder& string = data.string;
int32_t lengthPrefix = 0;
int32_t length1 = 0;
int32_t lengthInfix = 0;
int32_t length2 = 0;
int32_t lengthSuffix = 0;
// Use #define so that these are evaluated at the call site.
#define UPRV_INDEX_0 (lengthPrefix)
#define UPRV_INDEX_1 (lengthPrefix + length1)
#define UPRV_INDEX_2 (lengthPrefix + length1 + lengthInfix)
#define UPRV_INDEX_3 (lengthPrefix + length1 + lengthInfix + length2)
int32_t lengthRange = SimpleModifier::formatTwoArgPattern(
fRangeFormatter,
string,
0,
&lengthPrefix,
&lengthSuffix,
UNUM_FIELD_COUNT,
status);
if (U_FAILURE(status)) { return; }
lengthInfix = lengthRange - lengthPrefix - lengthSuffix;
U_ASSERT(lengthInfix > 0);
// SPACING HEURISTIC
// Add spacing unless all modifiers are collapsed.
// TODO: add API to control this?
// TODO: Use a data-driven heuristic like currency spacing?
// TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications)
{
bool repeatInner = !collapseInner && micros1.modInner->getCodePointCount() > 0;
bool repeatMiddle = !collapseMiddle && micros1.modMiddle->getCodePointCount() > 0;
bool repeatOuter = !collapseOuter && micros1.modOuter->getCodePointCount() > 0;
if (repeatInner || repeatMiddle || repeatOuter) {
// Add spacing if there is not already spacing
if (!PatternProps::isWhiteSpace(string.charAt(UPRV_INDEX_1))) {
lengthInfix += string.insertCodePoint(UPRV_INDEX_1, u'\u0020', UNUM_FIELD_COUNT, status);
}
if (!PatternProps::isWhiteSpace(string.charAt(UPRV_INDEX_2 - 1))) {
lengthInfix += string.insertCodePoint(UPRV_INDEX_2, u'\u0020', UNUM_FIELD_COUNT, status);
}
}
}
length1 += NumberFormatterImpl::writeNumber(micros1, data.quantity1, string, UPRV_INDEX_0, status);
length2 += NumberFormatterImpl::writeNumber(micros2, data.quantity2, string, UPRV_INDEX_2, status);
// TODO: Support padding?
if (collapseInner) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
const Modifier& mod = resolveModifierPlurals(*micros1.modInner, *micros2.modInner);
lengthInfix += mod.apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
} else {
length1 += micros1.modInner->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
length2 += micros2.modInner->apply(string, UPRV_INDEX_2, UPRV_INDEX_3, status);
}
if (collapseMiddle) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
const Modifier& mod = resolveModifierPlurals(*micros1.modMiddle, *micros2.modMiddle);
lengthInfix += mod.apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
} else {
length1 += micros1.modMiddle->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
length2 += micros2.modMiddle->apply(string, UPRV_INDEX_2, UPRV_INDEX_3, status);
}
if (collapseOuter) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
const Modifier& mod = resolveModifierPlurals(*micros1.modOuter, *micros2.modOuter);
lengthInfix += mod.apply(string, UPRV_INDEX_0, UPRV_INDEX_3, status);
} else {
length1 += micros1.modOuter->apply(string, UPRV_INDEX_0, UPRV_INDEX_1, status);
length2 += micros2.modOuter->apply(string, UPRV_INDEX_2, UPRV_INDEX_3, status);
}
}
const Modifier&
NumberRangeFormatterImpl::resolveModifierPlurals(const Modifier& first, const Modifier& second) const {
Modifier::Parameters parameters;
first.getParameters(parameters);
if (parameters.obj == nullptr) {
// No plural form; return a fallback (e.g., the first)
return first;
}
StandardPlural::Form firstPlural = parameters.plural;
second.getParameters(parameters);
if (parameters.obj == nullptr) {
// No plural form; return a fallback (e.g., the first)
return first;
}
StandardPlural::Form secondPlural = parameters.plural;
// Get the required plural form from data
StandardPlural::Form resultPlural = fPluralRanges.resolve(firstPlural, secondPlural);
// Get and return the new Modifier
const Modifier* mod = parameters.obj->getModifier(parameters.signum, resultPlural);
U_ASSERT(mod != nullptr);
return *mod;
}
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -0,0 +1,114 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
#include "unicode/utypes.h"
#if !UCONFIG_NO_FORMATTING
#ifndef __SOURCE_NUMRANGE_TYPES_H__
#define __SOURCE_NUMRANGE_TYPES_H__
#include "unicode/numberformatter.h"
#include "unicode/numberrangeformatter.h"
#include "unicode/simpleformatter.h"
#include "number_types.h"
#include "number_decimalquantity.h"
#include "number_formatimpl.h"
#include "number_stringbuilder.h"
U_NAMESPACE_BEGIN namespace number {
namespace impl {
/**
* Class similar to UFormattedNumberData.
*
* Has incomplete magic number logic that will need to be finished
* if this is to be exposed as C API in the future.
*/
struct UFormattedNumberRangeData : public UMemory {
// The magic number to identify incoming objects.
// Reads in ASCII as "FDR" (FormatteDnumberRange with room at the end)
static constexpr int32_t kMagic = 0x46445200;
// Data members:
int32_t fMagic = kMagic;
DecimalQuantity quantity1;
DecimalQuantity quantity2;
NumberStringBuilder string;
UNumberRangeIdentityResult identityResult = UNUM_IDENTITY_RESULT_COUNT;
// No C conversion methods (no C API yet)
};
class StandardPluralRanges : public UMemory {
public:
void initialize(const Locale& locale, UErrorCode& status);
StandardPlural::Form resolve(StandardPlural::Form first, StandardPlural::Form second) const;
/** Used for data loading. */
void addPluralRange(
StandardPlural::Form first,
StandardPlural::Form second,
StandardPlural::Form result);
/** Used for data loading. */
void setCapacity(int32_t length);
private:
struct StandardPluralRangeTriple {
StandardPlural::Form first;
StandardPlural::Form second;
StandardPlural::Form result;
};
// TODO: An array is simple here, but it results in linear lookup time.
// Certain locales have 20-30 entries in this list.
// Consider changing to a smarter data structure.
typedef MaybeStackArray<StandardPluralRangeTriple, 3> PluralRangeTriples;
PluralRangeTriples fTriples;
int32_t fTriplesLen = 0;
};
class NumberRangeFormatterImpl : public UMemory {
public:
NumberRangeFormatterImpl(const RangeMacroProps& macros, UErrorCode& status);
void format(UFormattedNumberRangeData& data, bool equalBeforeRounding, UErrorCode& status) const;
private:
NumberFormatterImpl formatterImpl1;
NumberFormatterImpl formatterImpl2;
bool fSameFormatters;
UNumberRangeCollapse fCollapse;
UNumberRangeIdentityFallback fIdentityFallback;
SimpleFormatter fRangeFormatter;
SimpleModifier fApproximatelyModifier;
StandardPluralRanges fPluralRanges;
void formatSingleValue(UFormattedNumberRangeData& data,
MicroProps& micros1, MicroProps& micros2,
UErrorCode& status) const;
void formatApproximately(UFormattedNumberRangeData& data,
MicroProps& micros1, MicroProps& micros2,
UErrorCode& status) const;
void formatRange(UFormattedNumberRangeData& data,
MicroProps& micros1, MicroProps& micros2,
UErrorCode& status) const;
const Modifier& resolveModifierPlurals(const Modifier& first, const Modifier& second) const;
};
} // namespace impl
} // namespace number
U_NAMESPACE_END
#endif //__SOURCE_NUMRANGE_TYPES_H__
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -144,6 +144,9 @@ class MultiplierFormatHandler;
class CurrencySymbols;
class GeneratorHelpers;
class DecNum;
class NumberRangeFormatterImpl;
struct RangeMacroProps;
void touchRangeLocales(impl::RangeMacroProps& macros);
} // namespace impl
@ -1423,7 +1426,8 @@ struct U_I18N_API MacroProps : public UMemory {
/**
* An abstract base class for specifying settings related to number formatting. This class is implemented by
* {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}.
* {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}. This class is not intended for
* public subclassing.
*/
template<typename Derived>
class U_I18N_API NumberFormatterSettings {
@ -2108,6 +2112,10 @@ class U_I18N_API NumberFormatterSettings {
friend class LocalizedNumberFormatter;
friend class UnlocalizedNumberFormatter;
// Give NumberRangeFormatter access to the MacroProps
friend void impl::touchRangeLocales(impl::RangeMacroProps& macros);
friend class impl::NumberRangeFormatterImpl;
};
/**
@ -2124,13 +2132,6 @@ class U_I18N_API UnlocalizedNumberFormatter
* Associate the given locale with the number formatter. The locale is used for picking the appropriate symbols,
* formats, and other data for number display.
*
* <p>
* To use the Java default locale, call Locale::getDefault():
*
* <pre>
* NumberFormatter::with(). ... .locale(Locale::getDefault())
* </pre>
*
* @param locale
* The locale to use when loading data for number formatting.
* @return The fluent chain.
@ -2156,7 +2157,6 @@ class U_I18N_API UnlocalizedNumberFormatter
*/
UnlocalizedNumberFormatter() = default;
// Make default copy constructor call the NumberFormatterSettings copy constructor.
/**
* Returns a copy of this UnlocalizedNumberFormatter.
* @draft ICU 60
@ -2295,7 +2295,6 @@ class U_I18N_API LocalizedNumberFormatter
*/
LocalizedNumberFormatter() = default;
// Make default copy constructor call the NumberFormatterSettings copy constructor.
/**
* Returns a copy of this LocalizedNumberFormatter.
* @draft ICU 60
@ -2457,9 +2456,9 @@ class U_I18N_API FormattedNumber : public UMemory {
#endif /* U_HIDE_DEPRECATED_API */
/**
* Determines the start and end indices of the next occurrence of the given <em>field</em> in the
* output string. This allows you to determine the locations of, for example, the integer part,
* fraction part, or symbols.
* Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given
* <em>field</em> in the output string. This allows you to determine the locations of, for example,
* the integer part, fraction part, or symbols.
*
* If a field occurs just once, calling this method will find that occurrence and return it. If a
* field occurs multiple times, this method may be called repeatedly with the following pattern:
@ -2478,7 +2477,7 @@ class U_I18N_API FormattedNumber : public UMemory {
* Input+output variable. On input, the "field" property determines which field to look
* up, and the "beginIndex" and "endIndex" properties determine where to begin the search.
* On output, the "beginIndex" is set to the beginning of the first occurrence of the
* field with either begin or end indices after the input indices, "endIndex" is set to
* field with either begin or end indices after the input indices; "endIndex" is set to
* the end of that occurrence of the field (exclusive index). If a field position is not
* found, the method returns FALSE and the FieldPosition may or may not be changed.
* @param status

View File

@ -0,0 +1,849 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
#if !UCONFIG_NO_FORMATTING
#ifndef __NUMBERRANGEFORMATTER_H__
#define __NUMBERRANGEFORMATTER_H__
#include "unicode/appendable.h"
#include "unicode/fieldpos.h"
#include "unicode/fpositer.h"
#include "unicode/numberformatter.h"
#ifndef U_HIDE_DRAFT_API
/**
* \file
* \brief C++ API: Library for localized formatting of number, currency, and unit ranges.
*
* The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement.
* <p>
* Usage example:
* <p>
* <pre>
* NumberRangeFormatter::with()
* .identityFallback(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE)
* .numberFormatterFirst(NumberFormatter::with().adoptUnit(MeasureUnit::createMeter()))
* .numberFormatterSecond(NumberFormatter::with().adoptUnit(MeasureUnit::createKilometer()))
* .locale("en-GB")
* .formatRange(750, 1.2, status)
* .toString(status);
* // => "750 m - 1.2 km"
* </pre>
* <p>
* Like NumberFormatter, NumberRangeFormatter instances are immutable and thread-safe. This API is based on the
* <em>fluent</em> design pattern popularized by libraries such as Google's Guava.
*
* @author Shane Carr
*/
/**
* Defines how to merge fields that are identical across the range sign.
*
* @draft ICU 63
*/
typedef enum UNumberRangeCollapse {
/**
* Use locale data and heuristics to determine how much of the string to collapse. Could end up collapsing none,
* some, or all repeated pieces in a locale-sensitive way.
*
* The heuristics used for this option are subject to change over time.
*
* @draft ICU 63
*/
UNUM_RANGE_COLLAPSE_AUTO,
/**
* Do not collapse any part of the number. Example: "3.2 thousand kilograms 5.3 thousand kilograms"
*
* @draft ICU 63
*/
UNUM_RANGE_COLLAPSE_NONE,
/**
* Collapse the unit part of the number, but not the notation, if present. Example: "3.2 thousand 5.3 thousand
* kilograms"
*
* @draft ICU 63
*/
UNUM_RANGE_COLLAPSE_UNIT,
/**
* Collapse any field that is equal across the range sign. May introduce ambiguity on the magnitude of the
* number. Example: "3.2 5.3 thousand kilograms"
*
* @draft ICU 63
*/
UNUM_RANGE_COLLAPSE_ALL
} UNumberRangeCollapse;
/**
* Defines the behavior when the two numbers in the range are identical after rounding. To programmatically detect
* when the identity fallback is used, compare the lower and upper BigDecimals via FormattedNumber.
*
* @draft ICU 63
* @see NumberRangeFormatter
*/
typedef enum UNumberRangeIdentityFallback {
/**
* Show the number as a single value rather than a range. Example: "$5"
*
* @draft ICU 63
*/
UNUM_IDENTITY_FALLBACK_SINGLE_VALUE,
/**
* Show the number using a locale-sensitive approximation pattern. If the numbers were the same before rounding,
* show the single value. Example: "~$5" or "$5"
*
* @draft ICU 63
*/
UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE,
/**
* Show the number using a locale-sensitive approximation pattern. Use the range pattern always, even if the
* inputs are the same. Example: "~$5"
*
* @draft ICU 63
*/
UNUM_IDENTITY_FALLBACK_APPROXIMATELY,
/**
* Show the number as the range of two equal values. Use the range pattern always, even if the inputs are the
* same. Example (with RangeCollapse.NONE): "$5 $5"
*
* @draft ICU 63
*/
UNUM_IDENTITY_FALLBACK_RANGE
} UNumberRangeIdentityFallback;
/**
* Used in the result class FormattedNumberRange to indicate to the user whether the numbers formatted in the range
* were equal or not, and whether or not the identity fallback was applied.
*
* @draft ICU 63
* @see NumberRangeFormatter
*/
typedef enum UNumberRangeIdentityResult {
/**
* Used to indicate that the two numbers in the range were equal, even before any rounding rules were applied.
*
* @draft ICU 63
* @see NumberRangeFormatter
*/
UNUM_IDENTITY_RESULT_EQUAL_BEFORE_ROUNDING,
/**
* Used to indicate that the two numbers in the range were equal, but only after rounding rules were applied.
*
* @draft ICU 63
* @see NumberRangeFormatter
*/
UNUM_IDENTITY_RESULT_EQUAL_AFTER_ROUNDING,
/**
* Used to indicate that the two numbers in the range were not equal, even after rounding rules were applied.
*
* @draft ICU 63
* @see NumberRangeFormatter
*/
UNUM_IDENTITY_RESULT_NOT_EQUAL,
#ifndef U_HIDE_INTERNAL_API
/**
* The number of entries in this enum.
* @internal
*/
UNUM_IDENTITY_RESULT_COUNT
#endif
} UNumberRangeIdentityResult;
U_NAMESPACE_BEGIN
namespace number { // icu::number
// Forward declarations:
class UnlocalizedNumberRangeFormatter;
class LocalizedNumberRangeFormatter;
class FormattedNumberRange;
namespace impl {
// Forward declarations:
struct RangeMacroProps;
class DecimalQuantity;
struct UFormattedNumberRangeData;
class NumberRangeFormatterImpl;
} // namespace impl
// Other helper classes would go here, but there are none.
namespace impl { // icu::number::impl
// Do not enclose entire MacroProps with #ifndef U_HIDE_INTERNAL_API, needed for a protected field
/** @internal */
struct U_I18N_API RangeMacroProps : public UMemory {
/** @internal */
UnlocalizedNumberFormatter formatter1; // = NumberFormatter::with();
/** @internal */
UnlocalizedNumberFormatter formatter2; // = NumberFormatter::with();
/** @internal */
bool singleFormatter = true;
/** @internal */
UNumberRangeCollapse collapse = UNUM_RANGE_COLLAPSE_AUTO;
/** @internal */
UNumberRangeIdentityFallback identityFallback = UNUM_IDENTITY_FALLBACK_APPROXIMATELY;
/** @internal */
Locale locale;
// NOTE: Uses default copy and move constructors.
/**
* Check all members for errors.
* @internal
*/
bool copyErrorTo(UErrorCode &status) const {
return formatter1.copyErrorTo(status) || formatter2.copyErrorTo(status);
}
};
} // namespace impl
/**
* An abstract base class for specifying settings related to number formatting. This class is implemented by
* {@link UnlocalizedNumberRangeFormatter} and {@link LocalizedNumberRangeFormatter}. This class is not intended for
* public subclassing.
*/
template<typename Derived>
class U_I18N_API NumberRangeFormatterSettings {
public:
/**
* Sets the NumberFormatter instance to use for the numbers in the range. The same formatter is applied to both
* sides of the range.
* <p>
* The NumberFormatter instances must not have a locale applied yet; the locale specified on the
* NumberRangeFormatter will be used.
*
* @param formatter
* The formatter to use for both numbers in the range.
* @return The fluent chain.
* @draft ICU 63
*/
Derived numberFormatterBoth(const UnlocalizedNumberFormatter &formatter) const &;
/**
* Overload of numberFormatterBoth() for use on an rvalue reference.
*
* @param formatter
* The formatter to use for both numbers in the range.
* @return The fluent chain.
* @see #numberFormatterBoth
* @draft ICU 63
*/
Derived numberFormatterBoth(const UnlocalizedNumberFormatter &formatter) &&;
/**
* Overload of numberFormatterBoth() for use on an rvalue reference.
*
* @param formatter
* The formatter to use for both numbers in the range.
* @return The fluent chain.
* @see #numberFormatterBoth
* @draft ICU 63
*/
Derived numberFormatterBoth(UnlocalizedNumberFormatter &&formatter) const &;
/**
* Overload of numberFormatterBoth() for use on an rvalue reference.
*
* @param formatter
* The formatter to use for both numbers in the range.
* @return The fluent chain.
* @see #numberFormatterBoth
* @draft ICU 63
*/
Derived numberFormatterBoth(UnlocalizedNumberFormatter &&formatter) &&;
/**
* Sets the NumberFormatter instance to use for the first number in the range.
* <p>
* The NumberFormatter instances must not have a locale applied yet; the locale specified on the
* NumberRangeFormatter will be used.
*
* @param formatterFirst
* The formatter to use for the first number in the range.
* @return The fluent chain.
* @draft ICU 63
*/
Derived numberFormatterFirst(const UnlocalizedNumberFormatter &formatterFirst) const &;
/**
* Overload of numberFormatterFirst() for use on an rvalue reference.
*
* @param formatterFirst
* The formatter to use for the first number in the range.
* @return The fluent chain.
* @see #numberFormatterFirst
* @draft ICU 63
*/
Derived numberFormatterFirst(const UnlocalizedNumberFormatter &formatterFirst) &&;
/**
* Overload of numberFormatterFirst() for use on an rvalue reference.
*
* @param formatterFirst
* The formatter to use for the first number in the range.
* @return The fluent chain.
* @see #numberFormatterFirst
* @draft ICU 63
*/
Derived numberFormatterFirst(UnlocalizedNumberFormatter &&formatterFirst) const &;
/**
* Overload of numberFormatterFirst() for use on an rvalue reference.
*
* @param formatterFirst
* The formatter to use for the first number in the range.
* @return The fluent chain.
* @see #numberFormatterFirst
* @draft ICU 63
*/
Derived numberFormatterFirst(UnlocalizedNumberFormatter &&formatterFirst) &&;
/**
* Sets the NumberFormatter instance to use for the second number in the range.
* <p>
* The NumberFormatter instances must not have a locale applied yet; the locale specified on the
* NumberRangeFormatter will be used.
*
* @param formatterSecond
* The formatter to use for the second number in the range.
* @return The fluent chain.
* @draft ICU 63
*/
Derived numberFormatterSecond(const UnlocalizedNumberFormatter &formatterSecond) const &;
/**
* Overload of numberFormatterSecond() for use on an rvalue reference.
*
* @param formatterSecond
* The formatter to use for the second number in the range.
* @return The fluent chain.
* @see #numberFormatterSecond
* @draft ICU 63
*/
Derived numberFormatterSecond(const UnlocalizedNumberFormatter &formatterSecond) &&;
/**
* Overload of numberFormatterSecond() for use on an rvalue reference.
*
* @param formatterSecond
* The formatter to use for the second number in the range.
* @return The fluent chain.
* @see #numberFormatterSecond
* @draft ICU 63
*/
Derived numberFormatterSecond(UnlocalizedNumberFormatter &&formatterSecond) const &;
/**
* Overload of numberFormatterSecond() for use on an rvalue reference.
*
* @param formatterSecond
* The formatter to use for the second number in the range.
* @return The fluent chain.
* @see #numberFormatterSecond
* @draft ICU 63
*/
Derived numberFormatterSecond(UnlocalizedNumberFormatter &&formatterSecond) &&;
/**
* Sets the aggressiveness of "collapsing" fields across the range separator. Possible values:
* <p>
* <ul>
* <li>ALL: "3-5K miles"</li>
* <li>UNIT: "3K - 5K miles"</li>
* <li>NONE: "3K miles - 5K miles"</li>
* <li>AUTO: usually UNIT or NONE, depending on the locale and formatter settings</li>
* </ul>
* <p>
* The default value is AUTO.
*
* @param collapse
* The collapsing strategy to use for this range.
* @return The fluent chain.
* @draft ICU 63
*/
Derived collapse(UNumberRangeCollapse collapse) const &;
/**
* Overload of collapse() for use on an rvalue reference.
*
* @param collapse
* The collapsing strategy to use for this range.
* @return The fluent chain.
* @see #collapse
* @draft ICU 63
*/
Derived collapse(UNumberRangeCollapse collapse) &&;
/**
* Sets the behavior when the two sides of the range are the same. This could happen if the same two numbers are
* passed to the formatRange function, or if different numbers are passed to the function but they become the same
* after rounding rules are applied. Possible values:
* <p>
* <ul>
* <li>SINGLE_VALUE: "5 miles"</li>
* <li>APPROXIMATELY_OR_SINGLE_VALUE: "~5 miles" or "5 miles", depending on whether the number was the same before
* rounding was applied</li>
* <li>APPROXIMATELY: "~5 miles"</li>
* <li>RANGE: "5-5 miles" (with collapse=UNIT)</li>
* </ul>
* <p>
* The default value is APPROXIMATELY.
*
* @param identityFallback
* The strategy to use when formatting two numbers that end up being the same.
* @return The fluent chain.
* @draft ICU 63
*/
Derived identityFallback(UNumberRangeIdentityFallback identityFallback) const &;
/**
* Overload of identityFallback() for use on an rvalue reference.
*
* @param identityFallback
* The strategy to use when formatting two numbers that end up being the same.
* @return The fluent chain.
* @see #identityFallback
* @draft ICU 63
*/
Derived identityFallback(UNumberRangeIdentityFallback identityFallback) &&;
/**
* Sets the UErrorCode if an error occurred in the fluent chain.
* Preserves older error codes in the outErrorCode.
* @return TRUE if U_FAILURE(outErrorCode)
* @draft ICU 63
*/
UBool copyErrorTo(UErrorCode &outErrorCode) const {
if (U_FAILURE(outErrorCode)) {
// Do not overwrite the older error code
return TRUE;
}
fMacros.copyErrorTo(outErrorCode);
return U_FAILURE(outErrorCode);
};
// NOTE: Uses default copy and move constructors.
private:
impl::RangeMacroProps fMacros;
// Don't construct me directly! Use (Un)LocalizedNumberFormatter.
NumberRangeFormatterSettings() = default;
friend class LocalizedNumberRangeFormatter;
friend class UnlocalizedNumberRangeFormatter;
};
/**
* A NumberRangeFormatter that does not yet have a locale. In order to format, a locale must be specified.
*
* @see NumberRangeFormatter
* @draft ICU 63
*/
class U_I18N_API UnlocalizedNumberRangeFormatter
: public NumberRangeFormatterSettings<UnlocalizedNumberRangeFormatter>, public UMemory {
public:
/**
* Associate the given locale with the number range formatter. The locale is used for picking the
* appropriate symbols, formats, and other data for number display.
*
* @param locale
* The locale to use when loading data for number formatting.
* @return The fluent chain.
* @draft ICU 63
*/
LocalizedNumberRangeFormatter locale(const icu::Locale &locale) const &;
/**
* Overload of locale() for use on an rvalue reference.
*
* @param locale
* The locale to use when loading data for number formatting.
* @return The fluent chain.
* @see #locale
* @draft ICU 63
*/
LocalizedNumberRangeFormatter locale(const icu::Locale &locale) &&;
/**
* Default constructor: puts the formatter into a valid but undefined state.
*
* @draft ICU 63
*/
UnlocalizedNumberRangeFormatter() = default;
/**
* Returns a copy of this UnlocalizedNumberRangeFormatter.
* @draft ICU 63
*/
UnlocalizedNumberRangeFormatter(const UnlocalizedNumberRangeFormatter &other);
/**
* Move constructor:
* The source UnlocalizedNumberRangeFormatter will be left in a valid but undefined state.
* @draft ICU 63
*/
UnlocalizedNumberRangeFormatter(UnlocalizedNumberRangeFormatter&& src) U_NOEXCEPT;
/**
* Copy assignment operator.
* @draft ICU 63
*/
UnlocalizedNumberRangeFormatter& operator=(const UnlocalizedNumberRangeFormatter& other);
/**
* Move assignment operator:
* The source UnlocalizedNumberRangeFormatter will be left in a valid but undefined state.
* @draft ICU 63
*/
UnlocalizedNumberRangeFormatter& operator=(UnlocalizedNumberRangeFormatter&& src) U_NOEXCEPT;
private:
explicit UnlocalizedNumberRangeFormatter(
const NumberRangeFormatterSettings<UnlocalizedNumberRangeFormatter>& other);
explicit UnlocalizedNumberRangeFormatter(
NumberRangeFormatterSettings<UnlocalizedNumberRangeFormatter>&& src) U_NOEXCEPT;
// To give the fluent setters access to this class's constructor:
friend class NumberRangeFormatterSettings<UnlocalizedNumberRangeFormatter>;
// To give NumberRangeFormatter::with() access to this class's constructor:
friend class NumberRangeFormatter;
};
/**
* A NumberRangeFormatter that has a locale associated with it; this means .formatRange() methods are available.
*
* @see NumberFormatter
* @draft ICU 63
*/
class U_I18N_API LocalizedNumberRangeFormatter
: public NumberRangeFormatterSettings<LocalizedNumberRangeFormatter>, public UMemory {
public:
/**
* Format the given Formattables to a string using the settings specified in the NumberRangeFormatter fluent setting
* chain.
*
* @param first
* The first number in the range, usually to the left in LTR locales.
* @param second
* The second number in the range, usually to the right in LTR locales.
* @param status
* Set if an error occurs while formatting.
* @return A FormattedNumberRange object; call .toString() to get the string.
* @draft ICU 63
*/
FormattedNumberRange formatFormattableRange(
const Formattable& first, const Formattable& second, UErrorCode& status) const;
/**
* Default constructor: puts the formatter into a valid but undefined state.
*
* @draft ICU 63
*/
LocalizedNumberRangeFormatter() = default;
/**
* Returns a copy of this LocalizedNumberRangeFormatter.
* @draft ICU 63
*/
LocalizedNumberRangeFormatter(const LocalizedNumberRangeFormatter &other);
/**
* Move constructor:
* The source LocalizedNumberRangeFormatter will be left in a valid but undefined state.
* @draft ICU 63
*/
LocalizedNumberRangeFormatter(LocalizedNumberRangeFormatter&& src) U_NOEXCEPT;
/**
* Copy assignment operator.
* @draft ICU 63
*/
LocalizedNumberRangeFormatter& operator=(const LocalizedNumberRangeFormatter& other);
/**
* Move assignment operator:
* The source LocalizedNumberRangeFormatter will be left in a valid but undefined state.
* @draft ICU 63
*/
LocalizedNumberRangeFormatter& operator=(LocalizedNumberRangeFormatter&& src) U_NOEXCEPT;
#ifndef U_HIDE_INTERNAL_API
/**
* @param results
* The results object. This method will mutate it to save the results.
* @param status
* Set if an error occurs while formatting.
* @internal
*/
void formatImpl(impl::UFormattedNumberRangeData& results, bool equalBeforeRounding,
UErrorCode& status) const;
#endif
/**
* Destruct this LocalizedNumberRangeFormatter, cleaning up any memory it might own.
* @draft ICU 63
*/
~LocalizedNumberRangeFormatter();
private:
// TODO: This is not thread-safe! Do NOT check this in without an atomic here.
impl::NumberRangeFormatterImpl* fImpl = nullptr;
explicit LocalizedNumberRangeFormatter(
const NumberRangeFormatterSettings<LocalizedNumberRangeFormatter>& other);
explicit LocalizedNumberRangeFormatter(
NumberRangeFormatterSettings<LocalizedNumberRangeFormatter>&& src) U_NOEXCEPT;
LocalizedNumberRangeFormatter(const impl::RangeMacroProps &macros, const Locale &locale);
LocalizedNumberRangeFormatter(impl::RangeMacroProps &&macros, const Locale &locale);
// To give the fluent setters access to this class's constructor:
friend class NumberRangeFormatterSettings<UnlocalizedNumberRangeFormatter>;
friend class NumberRangeFormatterSettings<LocalizedNumberRangeFormatter>;
// To give UnlocalizedNumberRangeFormatter::locale() access to this class's constructor:
friend class UnlocalizedNumberRangeFormatter;
};
/**
* The result of a number range formatting operation. This class allows the result to be exported in several data types,
* including a UnicodeString and a FieldPositionIterator.
*
* @draft ICU 63
*/
class U_I18N_API FormattedNumberRange : public UMemory {
public:
/**
* Returns a UnicodeString representation of the formatted number range.
*
* @param status
* Set if an error occurs while formatting the number to the UnicodeString.
* @return a UnicodeString containing the localized number range.
* @draft ICU 63
*/
UnicodeString toString(UErrorCode& status) const;
/**
* Appends the formatted number range to an Appendable.
*
* @param appendable
* The Appendable to which to append the formatted number range string.
* @param status
* Set if an error occurs while formatting the number range to the Appendable.
* @return The same Appendable, for chaining.
* @draft ICU 63
* @see Appendable
*/
Appendable &appendTo(Appendable &appendable, UErrorCode& status) const;
/**
* Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given
* <em>field</em> in the output string. This allows you to determine the locations of, for example,
* the integer part, fraction part, or symbols.
*
* If both sides of the range have the same field, the field will occur twice, once before the
* range separator and once after the range separator, if applicable.
*
* If a field occurs just once, calling this method will find that occurrence and return it. If a
* field occurs multiple times, this method may be called repeatedly with the following pattern:
*
* <pre>
* FieldPosition fpos(UNUM_INTEGER_FIELD);
* while (formattedNumberRange.nextFieldPosition(fpos, status)) {
* // do something with fpos.
* }
* </pre>
*
* This method is useful if you know which field to query. If you want all available field position
* information, use #getAllFieldPositions().
*
* @param fieldPosition
* Input+output variable. See {@link FormattedNumber#nextFieldPosition}.
* @param status
* Set if an error occurs while populating the FieldPosition.
* @return TRUE if a new occurrence of the field was found; FALSE otherwise.
* @draft ICU 63
* @see UNumberFormatFields
*/
UBool nextFieldPosition(FieldPosition& fieldPosition, UErrorCode& status) const;
/**
* Export the formatted number range to a FieldPositionIterator. This allows you to determine which characters in
* the output string correspond to which <em>fields</em>, such as the integer part, fraction part, and sign.
*
* If information on only one field is needed, use #nextFieldPosition() instead.
*
* @param iterator
* The FieldPositionIterator to populate with all of the fields present in the formatted number.
* @param status
* Set if an error occurs while populating the FieldPositionIterator.
* @draft ICU 63
* @see UNumberFormatFields
*/
void getAllFieldPositions(FieldPositionIterator &iterator, UErrorCode &status) const;
/**
* Export the first formatted number as a decimal number. This endpoint
* is useful for obtaining the exact number being printed after scaling
* and rounding have been applied by the number range formatting pipeline.
*
* The syntax of the unformatted number is a "numeric string"
* as defined in the Decimal Arithmetic Specification, available at
* http://speleotrove.com/decimal
*
* @return A decimal representation of the first formatted number.
* @draft ICU 63
* @see NumberRangeFormatter
* @see #getSecondDecimal
*/
UnicodeString getFirstDecimal(UErrorCode& status) const;
/**
* Export the second formatted number as a decimal number. This endpoint
* is useful for obtaining the exact number being printed after scaling
* and rounding have been applied by the number range formatting pipeline.
*
* The syntax of the unformatted number is a "numeric string"
* as defined in the Decimal Arithmetic Specification, available at
* http://speleotrove.com/decimal
*
* @return A decimal representation of the second formatted number.
* @draft ICU 63
* @see NumberRangeFormatter
* @see #getFirstDecimal
*/
UnicodeString getSecondDecimal(UErrorCode& status) const;
/**
* Returns whether the pair of numbers was successfully formatted as a range or whether an identity fallback was
* used. For example, if the first and second number were the same either before or after rounding occurred, an
* identity fallback was used.
*
* @return An indication the resulting identity situation in the formatted number range.
* @draft ICU 63
* @see UNumberRangeIdentityFallback
*/
UNumberRangeIdentityResult getIdentityResult(UErrorCode& status) const;
/**
* Copying not supported; use move constructor instead.
*/
FormattedNumberRange(const FormattedNumberRange&) = delete;
/**
* Copying not supported; use move assignment instead.
*/
FormattedNumberRange& operator=(const FormattedNumberRange&) = delete;
/**
* Move constructor:
* Leaves the source FormattedNumberRange in an undefined state.
* @draft ICU 63
*/
FormattedNumberRange(FormattedNumberRange&& src) U_NOEXCEPT;
/**
* Move assignment:
* Leaves the source FormattedNumberRange in an undefined state.
* @draft ICU 63
*/
FormattedNumberRange& operator=(FormattedNumberRange&& src) U_NOEXCEPT;
/**
* Destruct an instance of FormattedNumberRange, cleaning up any memory it might own.
* @draft ICU 63
*/
~FormattedNumberRange();
private:
// Can't use LocalPointer because UFormattedNumberRangeData is forward-declared
const impl::UFormattedNumberRangeData *fResults;
// Error code for the terminal methods
UErrorCode fErrorCode;
/**
* Internal constructor from data type. Adopts the data pointer.
* @internal
*/
explicit FormattedNumberRange(impl::UFormattedNumberRangeData *results)
: fResults(results), fErrorCode(U_ZERO_ERROR) {};
explicit FormattedNumberRange(UErrorCode errorCode)
: fResults(nullptr), fErrorCode(errorCode) {};
void getAllFieldPositionsImpl(FieldPositionIteratorHandler& fpih, UErrorCode& status) const;
// To give LocalizedNumberRangeFormatter format methods access to this class's constructor:
friend class LocalizedNumberRangeFormatter;
};
/**
* See the main description in numberrangeformatter.h for documentation and examples.
*
* @draft ICU 63
*/
class U_I18N_API NumberRangeFormatter final {
public:
/**
* Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is not currently
* known at the call site.
*
* @return An {@link UnlocalizedNumberRangeFormatter}, to be used for chaining.
* @draft ICU 63
*/
static UnlocalizedNumberRangeFormatter with();
/**
* Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is known at the call
* site.
*
* @param locale
* The locale from which to load formats and symbols for number range formatting.
* @return A {@link LocalizedNumberRangeFormatter}, to be used for chaining.
* @draft ICU 63
*/
static LocalizedNumberRangeFormatter withLocale(const Locale &locale);
/**
* Use factory methods instead of the constructor to create a NumberFormatter.
*/
NumberRangeFormatter() = delete;
};
} // namespace number
U_NAMESPACE_END
#endif // U_HIDE_DRAFT_API
#endif // __NUMBERRANGEFORMATTER_H__
#endif /* #if !UCONFIG_NO_FORMATTING */

View File

@ -66,7 +66,7 @@ numbertest_affixutils.o numbertest_api.o numbertest_decimalquantity.o \
numbertest_modifiers.o numbertest_patternmodifier.o numbertest_patternstring.o \
numbertest_stringbuilder.o numbertest_stringsegment.o \
numbertest_parse.o numbertest_doubleconversion.o numbertest_skeletons.o \
static_unisets_test.o numfmtdatadriventest.o erarulestest.o
static_unisets_test.o numfmtdatadriventest.o numbertest_range.o erarulestest.o
DEPS = $(OBJECTS:.o=.d)

View File

@ -256,6 +256,7 @@
<ClCompile Include="numbertest_parse.cpp" />
<ClCompile Include="numbertest_doubleconversion.cpp" />
<ClCompile Include="numbertest_skeletons.cpp" />
<ClCompile Include="numbertest_range.cpp" />
<ClCompile Include="numfmtst.cpp" />
<ClCompile Include="numfmtdatadriventest.cpp" />
<ClCompile Include="numrgts.cpp" />

View File

@ -289,6 +289,9 @@
<ClCompile Include="numbertest_skeletons.cpp">
<Filter>formatting</Filter>
</ClCompile>
<ClCompile Include="numbertest_range.cpp">
<Filter>formatting</Filter>
</ClCompile>
<ClCompile Include="numfmtst.cpp">
<Filter>formatting</Filter>
</ClCompile>

View File

@ -11,6 +11,8 @@
#include "number_affixutils.h"
#include "numparse_stringsegment.h"
#include "unicode/locid.h"
#include "unicode/numberformatter.h"
#include "unicode/numberrangeformatter.h"
using namespace icu::number;
using namespace icu::number::impl;
@ -244,6 +246,53 @@ class NumberSkeletonTest : public IntlTest {
void expectedErrorSkeleton(const char16_t** cases, int32_t casesLen);
};
class NumberRangeFormatterTest : public IntlTest {
public:
NumberRangeFormatterTest();
NumberRangeFormatterTest(UErrorCode &status);
void testSanity();
void testBasic();
void testCollapse();
void testIdentity();
void testDifferentFormatters();
void testPlurals();
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par = 0);
private:
CurrencyUnit USD;
CurrencyUnit GBP;
CurrencyUnit PTE;
MeasureUnit METER;
MeasureUnit KILOMETER;
MeasureUnit FAHRENHEIT;
MeasureUnit KELVIN;
void assertFormatRange(
const char16_t* message,
const UnlocalizedNumberRangeFormatter& f,
Locale locale,
const char16_t* expected_10_50,
const char16_t* expected_49_51,
const char16_t* expected_50_50,
const char16_t* expected_00_30,
const char16_t* expected_00_00,
const char16_t* expected_30_3K,
const char16_t* expected_30K_50K,
const char16_t* expected_49K_51K,
const char16_t* expected_50K_50K,
const char16_t* expected_50K_50M);
void assertFormattedRangeEquals(
const char16_t* message,
const LocalizedNumberRangeFormatter& l,
double first,
double second,
const char16_t* expected);
};
// NOTE: This macro is identical to the one in itformat.cpp
#define TESTCLASS(id, TestClass) \
@ -276,6 +325,7 @@ class NumberTest : public IntlTest {
TESTCLASS(8, StringSegmentTest);
TESTCLASS(9, NumberParserTest);
TESTCLASS(10, NumberSkeletonTest);
TESTCLASS(11, NumberRangeFormatterTest);
default: name = ""; break; // needed to end loop
}
}

View File

@ -17,6 +17,7 @@
#include "unicode/utypes.h"
// Horrible workaround for the lack of a status code in the constructor...
// (Also affects numbertest_range.cpp)
UErrorCode globalNumberFormatterApiTestStatus = U_ZERO_ERROR;
NumberFormatterApiTest::NumberFormatterApiTest()

View File

@ -162,12 +162,12 @@ void ModifiersTest::assertModifierEquals(const Modifier &mod, NumberStringBuilde
UErrorCode &status) {
int32_t oldCount = sb.codePointCount();
mod.apply(sb, 0, sb.length(), status);
assertEquals("Prefix length", expectedPrefixLength, mod.getPrefixLength(status));
assertEquals("Prefix length", expectedPrefixLength, mod.getPrefixLength());
assertEquals("Strong", expectedStrong, mod.isStrong());
if (dynamic_cast<const CurrencySpacingEnabledModifier*>(&mod) == nullptr) {
// i.e., if mod is not a CurrencySpacingEnabledModifier
assertEquals("Code point count equals actual code point count",
sb.codePointCount() - oldCount, mod.getCodePointCount(status));
sb.codePointCount() - oldCount, mod.getCodePointCount());
}
UnicodeString debugString;

View File

@ -170,14 +170,14 @@ void PatternModifierTest::testMutableEqualsImmutable() {
UnicodeString PatternModifierTest::getPrefix(const MutablePatternModifier &mod, UErrorCode &status) {
NumberStringBuilder nsb;
mod.apply(nsb, 0, 0, status);
int32_t prefixLength = mod.getPrefixLength(status);
int32_t prefixLength = mod.getPrefixLength();
return UnicodeString(nsb.toUnicodeString(), 0, prefixLength);
}
UnicodeString PatternModifierTest::getSuffix(const MutablePatternModifier &mod, UErrorCode &status) {
NumberStringBuilder nsb;
mod.apply(nsb, 0, 0, status);
int32_t prefixLength = mod.getPrefixLength(status);
int32_t prefixLength = mod.getPrefixLength();
return UnicodeString(nsb.toUnicodeString(), prefixLength, nsb.length() - prefixLength);
}

View File

@ -0,0 +1,750 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
#include "unicode/utypes.h"
#if !UCONFIG_NO_FORMATTING
#include "numbertest.h"
#include "unicode/numberrangeformatter.h"
#include <cmath>
#include <numparse_affixes.h>
// Horrible workaround for the lack of a status code in the constructor...
// (Also affects numbertest_api.cpp)
UErrorCode globalNumberRangeFormatterTestStatus = U_ZERO_ERROR;
NumberRangeFormatterTest::NumberRangeFormatterTest()
: NumberRangeFormatterTest(globalNumberRangeFormatterTestStatus) {
}
NumberRangeFormatterTest::NumberRangeFormatterTest(UErrorCode& status)
: USD(u"USD", status),
GBP(u"GBP", status),
PTE(u"PTE", status) {
// Check for error on the first MeasureUnit in case there is no data
LocalPointer<MeasureUnit> unit(MeasureUnit::createMeter(status));
if (U_FAILURE(status)) {
dataerrln("%s %d status = %s", __FILE__, __LINE__, u_errorName(status));
return;
}
METER = *unit;
KILOMETER = *LocalPointer<MeasureUnit>(MeasureUnit::createKilometer(status));
FAHRENHEIT = *LocalPointer<MeasureUnit>(MeasureUnit::createFahrenheit(status));
KELVIN = *LocalPointer<MeasureUnit>(MeasureUnit::createKelvin(status));
}
void NumberRangeFormatterTest::runIndexedTest(int32_t index, UBool exec, const char*& name, char*) {
if (exec) {
logln("TestSuite NumberRangeFormatterTest: ");
}
TESTCASE_AUTO_BEGIN;
TESTCASE_AUTO(testSanity);
TESTCASE_AUTO(testBasic);
TESTCASE_AUTO(testCollapse);
TESTCASE_AUTO(testIdentity);
TESTCASE_AUTO(testDifferentFormatters);
TESTCASE_AUTO(testPlurals);
TESTCASE_AUTO_END;
}
void NumberRangeFormatterTest::testSanity() {
IcuTestErrorCode status(*this, "testSanity");
LocalizedNumberRangeFormatter lnrf1 = NumberRangeFormatter::withLocale("en-us");
LocalizedNumberRangeFormatter lnrf2 = NumberRangeFormatter::with().locale("en-us");
assertEquals("Formatters should have same behavior 1",
lnrf1.formatFormattableRange(4, 6, status).toString(status),
lnrf2.formatFormattableRange(4, 6, status).toString(status));
}
void NumberRangeFormatterTest::testBasic() {
assertFormatRange(
u"Basic",
NumberRangeFormatter::with(),
Locale("en-us"),
u"15",
u"~5",
u"~5",
u"03",
u"~0",
u"33,000",
u"3,0005,000",
u"4,9995,001",
u"~5,000",
u"5,0005,000,000");
assertFormatRange(
u"Basic with units",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(METER)),
Locale("en-us"),
u"15 m",
u"~5 m",
u"~5 m",
u"03 m",
u"~0 m",
u"33,000 m",
u"3,0005,000 m",
u"4,9995,001 m",
u"~5,000 m",
u"5,0005,000,000 m");
assertFormatRange(
u"Basic with different units",
NumberRangeFormatter::with()
.numberFormatterFirst(NumberFormatter::with().unit(METER))
.numberFormatterSecond(NumberFormatter::with().unit(KILOMETER)),
Locale("en-us"),
u"1 m 5 km",
u"5 m 5 km",
u"5 m 5 km",
u"0 m 3 km",
u"0 m 0 km",
u"3 m 3,000 km",
u"3,000 m 5,000 km",
u"4,999 m 5,001 km",
u"5,000 m 5,000 km",
u"5,000 m 5,000,000 km");
assertFormatRange(
u"Basic long unit",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(METER).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)),
Locale("en-us"),
u"15 meters",
u"~5 meters",
u"~5 meters",
u"03 meters",
u"~0 meters",
u"33,000 meters",
u"3,0005,000 meters",
u"4,9995,001 meters",
u"~5,000 meters",
u"5,0005,000,000 meters");
assertFormatRange(
u"Non-English locale and unit",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(FAHRENHEIT).unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)),
Locale("fr-FR"),
u"15 degrés Fahrenheit",
u"~5 degrés Fahrenheit",
u"~5 degrés Fahrenheit",
u"03 degrés Fahrenheit",
u"~0 degré Fahrenheit",
u"33 000 degrés Fahrenheit",
u"3 0005 000 degrés Fahrenheit",
u"4 9995 001 degrés Fahrenheit",
u"~5 000 degrés Fahrenheit",
u"5 0005 000 000 degrés Fahrenheit");
assertFormatRange(
u"Locale with custom range separator",
NumberRangeFormatter::with(),
Locale("ja"),
u"15",
u"~5",
u"~5",
u"03",
u"~0",
u"33,000",
u"3,0005,000",
u"4,9995,001",
u"~5,000",
u"5,0005,000,000");
assertFormatRange(
u"Locale that already has spaces around range separator",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_NONE)
.numberFormatterBoth(NumberFormatter::with().unit(KELVIN)),
Locale("hr"),
u"1 K 5 K",
u"~5 K",
u"~5 K",
u"0 K 3 K",
u"~0 K",
u"3 K 3.000 K",
u"3.000 K 5.000 K",
u"4.999 K 5.001 K",
u"~5.000 K",
u"5.000 K 5.000.000 K");
assertFormatRange(
u"Locale with custom numbering system and no plural ranges data",
NumberRangeFormatter::with(),
Locale("shn@numbers=beng"),
// 012459 = ০১৩৪৫৯
u"১–৫",
u"~৫",
u"~৫",
u"০–৩",
u"~",
u"৩–৩,",
u"৩,০০০–৫,",
u",৯৯৯–৫,০০১",
u"~৫,",
u"৫,০০০–৫,,");
assertFormatRange(
u"Portuguese currency",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(PTE)),
Locale("pt-PT"),
u"1$00 - 5$00 \u200B",
u"~5$00 \u200B",
u"~5$00 \u200B",
u"0$00 - 3$00 \u200B",
u"~0$00 \u200B",
u"3$00 - 3000$00 \u200B",
u"3000$00 - 5000$00 \u200B",
u"4999$00 - 5001$00 \u200B",
u"~5000$00 \u200B",
u"5000$00 - 5,000,000$00 \u200B");
}
void NumberRangeFormatterTest::testCollapse() {
assertFormatRange(
u"Default collapse on currency (default rounding)",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(USD)),
Locale("en-us"),
u"$1.00 $5.00",
u"~$5.00",
u"~$5.00",
u"$0.00 $3.00",
u"~$0.00",
u"$3.00 $3,000.00",
u"$3,000.00 $5,000.00",
u"$4,999.00 $5,001.00",
u"~$5,000.00",
u"$5,000.00 $5,000,000.00");
assertFormatRange(
u"Default collapse on currency",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())),
Locale("en-us"),
u"$1 $5",
u"~$5",
u"~$5",
u"$0 $3",
u"~$0",
u"$3 $3,000",
u"$3,000 $5,000",
u"$4,999 $5,001",
u"~$5,000",
u"$5,000 $5,000,000");
assertFormatRange(
u"No collapse on currency",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_NONE)
.numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())),
Locale("en-us"),
u"$1 $5",
u"~$5",
u"~$5",
u"$0 $3",
u"~$0",
u"$3 $3,000",
u"$3,000 $5,000",
u"$4,999 $5,001",
u"~$5,000",
u"$5,000 $5,000,000");
assertFormatRange(
u"Unit collapse on currency",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_UNIT)
.numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())),
Locale("en-us"),
u"$15",
u"~$5",
u"~$5",
u"$03",
u"~$0",
u"$33,000",
u"$3,0005,000",
u"$4,9995,001",
u"~$5,000",
u"$5,0005,000,000");
assertFormatRange(
u"All collapse on currency",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_ALL)
.numberFormatterBoth(NumberFormatter::with().unit(USD).precision(Precision::integer())),
Locale("en-us"),
u"$15",
u"~$5",
u"~$5",
u"$03",
u"~$0",
u"$33,000",
u"$3,0005,000",
u"$4,9995,001",
u"~$5,000",
u"$5,0005,000,000");
assertFormatRange(
u"Default collapse on currency ISO code",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with()
.unit(GBP)
.unitWidth(UNUM_UNIT_WIDTH_ISO_CODE)
.precision(Precision::integer())),
Locale("en-us"),
u"GBP 15",
u"~GBP 5", // TODO: Fix this at some point
u"~GBP 5",
u"GBP 03",
u"~GBP 0",
u"GBP 33,000",
u"GBP 3,0005,000",
u"GBP 4,9995,001",
u"~GBP 5,000",
u"GBP 5,0005,000,000");
assertFormatRange(
u"No collapse on currency ISO code",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_NONE)
.numberFormatterBoth(NumberFormatter::with()
.unit(GBP)
.unitWidth(UNUM_UNIT_WIDTH_ISO_CODE)
.precision(Precision::integer())),
Locale("en-us"),
u"GBP 1 GBP 5",
u"~GBP 5", // TODO: Fix this at some point
u"~GBP 5",
u"GBP 0 GBP 3",
u"~GBP 0",
u"GBP 3 GBP 3,000",
u"GBP 3,000 GBP 5,000",
u"GBP 4,999 GBP 5,001",
u"~GBP 5,000",
u"GBP 5,000 GBP 5,000,000");
assertFormatRange(
u"Unit collapse on currency ISO code",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_UNIT)
.numberFormatterBoth(NumberFormatter::with()
.unit(GBP)
.unitWidth(UNUM_UNIT_WIDTH_ISO_CODE)
.precision(Precision::integer())),
Locale("en-us"),
u"GBP 15",
u"~GBP 5", // TODO: Fix this at some point
u"~GBP 5",
u"GBP 03",
u"~GBP 0",
u"GBP 33,000",
u"GBP 3,0005,000",
u"GBP 4,9995,001",
u"~GBP 5,000",
u"GBP 5,0005,000,000");
assertFormatRange(
u"All collapse on currency ISO code",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_ALL)
.numberFormatterBoth(NumberFormatter::with()
.unit(GBP)
.unitWidth(UNUM_UNIT_WIDTH_ISO_CODE)
.precision(Precision::integer())),
Locale("en-us"),
u"GBP 15",
u"~GBP 5", // TODO: Fix this at some point
u"~GBP 5",
u"GBP 03",
u"~GBP 0",
u"GBP 33,000",
u"GBP 3,0005,000",
u"GBP 4,9995,001",
u"~GBP 5,000",
u"GBP 5,0005,000,000");
// Default collapse on measurement unit is in testBasic()
assertFormatRange(
u"No collapse on measurement unit",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_NONE)
.numberFormatterBoth(NumberFormatter::with().unit(METER)),
Locale("en-us"),
u"1 m 5 m",
u"~5 m",
u"~5 m",
u"0 m 3 m",
u"~0 m",
u"3 m 3,000 m",
u"3,000 m 5,000 m",
u"4,999 m 5,001 m",
u"~5,000 m",
u"5,000 m 5,000,000 m");
assertFormatRange(
u"Unit collapse on measurement unit",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_UNIT)
.numberFormatterBoth(NumberFormatter::with().unit(METER)),
Locale("en-us"),
u"15 m",
u"~5 m",
u"~5 m",
u"03 m",
u"~0 m",
u"33,000 m",
u"3,0005,000 m",
u"4,9995,001 m",
u"~5,000 m",
u"5,0005,000,000 m");
assertFormatRange(
u"All collapse on measurement unit",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_ALL)
.numberFormatterBoth(NumberFormatter::with().unit(METER)),
Locale("en-us"),
u"15 m",
u"~5 m",
u"~5 m",
u"03 m",
u"~0 m",
u"33,000 m",
u"3,0005,000 m",
u"4,9995,001 m",
u"~5,000 m",
u"5,0005,000,000 m");
assertFormatRange(
u"Default collapse, long-form compact notation",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactLong())),
Locale("de-CH"),
u"15",
u"~5",
u"~5",
u"03",
u"~0",
u"33 Tausend",
u"35 Tausend",
u"~5 Tausend",
u"~5 Tausend",
u"5 Tausend 5 Millionen");
assertFormatRange(
u"Unit collapse, long-form compact notation",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_UNIT)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactLong())),
Locale("de-CH"),
u"15",
u"~5",
u"~5",
u"03",
u"~0",
u"33 Tausend",
u"3 Tausend 5 Tausend",
u"~5 Tausend",
u"~5 Tausend",
u"5 Tausend 5 Millionen");
assertFormatRange(
u"Default collapse on measurement unit with compact-short notation",
NumberRangeFormatter::with()
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)),
Locale("en-us"),
u"15 m",
u"~5 m",
u"~5 m",
u"03 m",
u"~0 m",
u"33K m",
u"3K 5K m",
u"~5K m",
u"~5K m",
u"5K 5M m");
assertFormatRange(
u"No collapse on measurement unit with compact-short notation",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_NONE)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)),
Locale("en-us"),
u"1 m 5 m",
u"~5 m",
u"~5 m",
u"0 m 3 m",
u"~0 m",
u"3 m 3K m",
u"3K m 5K m",
u"~5K m",
u"~5K m",
u"5K m 5M m");
assertFormatRange(
u"Unit collapse on measurement unit with compact-short notation",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_UNIT)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)),
Locale("en-us"),
u"15 m",
u"~5 m",
u"~5 m",
u"03 m",
u"~0 m",
u"33K m",
u"3K 5K m",
u"~5K m",
u"~5K m",
u"5K 5M m");
assertFormatRange(
u"All collapse on measurement unit with compact-short notation",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_ALL)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort()).unit(METER)),
Locale("en-us"),
u"15 m",
u"~5 m",
u"~5 m",
u"03 m",
u"~0 m",
u"33K m",
u"35K m", // this one is the key use case for ALL
u"~5K m",
u"~5K m",
u"5K 5M m");
assertFormatRange(
u"No collapse on scientific notation",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_NONE)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::scientific())),
Locale("en-us"),
u"1E0 5E0",
u"~5E0",
u"~5E0",
u"0E0 3E0",
u"~0E0",
u"3E0 3E3",
u"3E3 5E3",
u"4.999E3 5.001E3",
u"~5E3",
u"5E3 5E6");
assertFormatRange(
u"All collapse on scientific notation",
NumberRangeFormatter::with()
.collapse(UNUM_RANGE_COLLAPSE_ALL)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::scientific())),
Locale("en-us"),
u"15E0",
u"~5E0",
u"~5E0",
u"03E0",
u"~0E0",
u"3E0 3E3",
u"35E3",
u"4.9995.001E3",
u"~5E3",
u"5E3 5E6");
// TODO: Test compact currency?
// The code is not smart enough to differentiate the notation from the unit.
}
void NumberRangeFormatterTest::testIdentity() {
assertFormatRange(
u"Identity fallback Range",
NumberRangeFormatter::with().identityFallback(UNUM_IDENTITY_FALLBACK_RANGE),
Locale("en-us"),
u"15",
u"55",
u"55",
u"03",
u"00",
u"33,000",
u"3,0005,000",
u"4,9995,001",
u"5,0005,000",
u"5,0005,000,000");
assertFormatRange(
u"Identity fallback Approximately or Single Value",
NumberRangeFormatter::with().identityFallback(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE),
Locale("en-us"),
u"15",
u"~5",
u"5",
u"03",
u"0",
u"33,000",
u"3,0005,000",
u"4,9995,001",
u"5,000",
u"5,0005,000,000");
assertFormatRange(
u"Identity fallback Single Value",
NumberRangeFormatter::with().identityFallback(UNUM_IDENTITY_FALLBACK_SINGLE_VALUE),
Locale("en-us"),
u"15",
u"5",
u"5",
u"03",
u"0",
u"33,000",
u"3,0005,000",
u"4,9995,001",
u"5,000",
u"5,0005,000,000");
assertFormatRange(
u"Identity fallback Approximately or Single Value with compact notation",
NumberRangeFormatter::with()
.identityFallback(UNUM_IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE)
.numberFormatterBoth(NumberFormatter::with().notation(Notation::compactShort())),
Locale("en-us"),
u"15",
u"~5",
u"5",
u"03",
u"0",
u"33K",
u"3K 5K",
u"~5K",
u"5K",
u"5K 5M");
}
void NumberRangeFormatterTest::testDifferentFormatters() {
assertFormatRange(
u"Different rounding rules",
NumberRangeFormatter::with()
.numberFormatterFirst(NumberFormatter::with().precision(Precision::integer()))
.numberFormatterSecond(NumberFormatter::with().precision(Precision::fixedDigits(2))),
Locale("en-us"),
u"15.0",
u"55.0",
u"55.0",
u"03.0",
u"00.0",
u"33,000",
u"3,0005,000",
u"4,9995,000",
u"5,0005,000", // TODO: Should this one be ~5,000?
u"5,0005,000,000");
}
void NumberRangeFormatterTest::testPlurals() {
IcuTestErrorCode status(*this, "testPlurals");
// Locale sl has interesting plural forms:
// GBP{
// one{"britanski funt"}
// two{"britanska funta"}
// few{"britanski funti"}
// other{"britanskih funtov"}
// }
Locale locale("sl");
UnlocalizedNumberFormatter unf = NumberFormatter::with()
.unit(GBP)
.unitWidth(UNUM_UNIT_WIDTH_FULL_NAME)
.precision(Precision::integer());
LocalizedNumberFormatter lnf = unf.locale(locale);
// For comparison, run the non-range version of the formatter
assertEquals(Int64ToUnicodeString(1), u"1 britanski funt", lnf.formatDouble(1, status).toString(status));
assertEquals(Int64ToUnicodeString(2), u"2 britanska funta", lnf.formatDouble(2, status).toString(status));
assertEquals(Int64ToUnicodeString(3), u"3 britanski funti", lnf.formatDouble(3, status).toString(status));
assertEquals(Int64ToUnicodeString(5), u"5 britanskih funtov", lnf.formatDouble(5, status).toString(status));
if (status.errIfFailureAndReset()) { return; }
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter::with()
.numberFormatterBoth(unf)
.identityFallback(UNUM_IDENTITY_FALLBACK_RANGE)
.locale(locale);
struct TestCase {
double first;
double second;
const char16_t* expected;
} cases[] = {
{1, 1, u"11 britanski funti"}, // one + one -> few
{1, 2, u"12 britanska funta"}, // one + two -> two
{1, 3, u"13 britanski funti"}, // one + few -> few
{1, 5, u"15 britanskih funtov"}, // one + other -> other
{2, 1, u"21 britanski funti"}, // two + one -> few
{2, 2, u"22 britanska funta"}, // two + two -> two
{2, 3, u"23 britanski funti"}, // two + few -> few
{2, 5, u"25 britanskih funtov"}, // two + other -> other
{3, 1, u"31 britanski funti"}, // few + one -> few
{3, 2, u"32 britanska funta"}, // few + two -> two
{3, 3, u"33 britanski funti"}, // few + few -> few
{3, 5, u"35 britanskih funtov"}, // few + other -> other
{5, 1, u"51 britanski funti"}, // other + one -> few
{5, 2, u"52 britanska funta"}, // other + two -> two
{5, 3, u"53 britanski funti"}, // other + few -> few
{5, 5, u"55 britanskih funtov"}, // other + other -> other
};
for (auto& cas : cases) {
UnicodeString message = Int64ToUnicodeString(cas.first);
message += u" ";
message += Int64ToUnicodeString(cas.second);
status.setScope(message);
UnicodeString actual = lnrf.formatFormattableRange(cas.first, cas.second, status).toString(status);
assertEquals(message, cas.expected, actual);
status.errIfFailureAndReset();
}
}
void NumberRangeFormatterTest::assertFormatRange(
const char16_t* message,
const UnlocalizedNumberRangeFormatter& f,
Locale locale,
const char16_t* expected_10_50,
const char16_t* expected_49_51,
const char16_t* expected_50_50,
const char16_t* expected_00_30,
const char16_t* expected_00_00,
const char16_t* expected_30_3K,
const char16_t* expected_30K_50K,
const char16_t* expected_49K_51K,
const char16_t* expected_50K_50K,
const char16_t* expected_50K_50M) {
LocalizedNumberRangeFormatter l = f.locale(locale);
assertFormattedRangeEquals(message, l, 1, 5, expected_10_50);
assertFormattedRangeEquals(message, l, 4.9999999, 5.0000001, expected_49_51);
assertFormattedRangeEquals(message, l, 5, 5, expected_50_50);
assertFormattedRangeEquals(message, l, 0, 3, expected_00_30);
assertFormattedRangeEquals(message, l, 0, 0, expected_00_00);
assertFormattedRangeEquals(message, l, 3, 3000, expected_30_3K);
assertFormattedRangeEquals(message, l, 3000, 5000, expected_30K_50K);
assertFormattedRangeEquals(message, l, 4999, 5001, expected_49K_51K);
assertFormattedRangeEquals(message, l, 5000, 5000, expected_50K_50K);
assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M);
}
void NumberRangeFormatterTest::assertFormattedRangeEquals(
const char16_t* message,
const LocalizedNumberRangeFormatter& l,
double first,
double second,
const char16_t* expected) {
IcuTestErrorCode status(*this, "assertFormattedRangeEquals");
UnicodeString fullMessage = UnicodeString(message) + u": " + DoubleToUnicodeString(first) + u", " + DoubleToUnicodeString(second);
status.setScope(fullMessage);
UnicodeString actual = l.formatFormattableRange(first, second, status).toString(status);
assertEquals(fullMessage, expected, actual);
}
#endif

View File

@ -5,10 +5,11 @@ package com.ibm.icu.impl.number;
import com.ibm.icu.impl.StandardPlural;
/**
* A ParameterizedModifier by itself is NOT a Modifier. Rather, it wraps a data structure containing two
* or more Modifiers and returns the modifier appropriate for the current situation.
* This implementation of ModifierStore adopts references to Modifiers.
*
* (This is named "adopting" because in C++, this class takes ownership of the Modifiers.)
*/
public class ParameterizedModifier {
public class AdoptingModifierStore implements ModifierStore {
private final Modifier positive;
private final Modifier zero;
private final Modifier negative;
@ -21,7 +22,7 @@ public class ParameterizedModifier {
* <p>
* If this constructor is used, a plural form CANNOT be passed to {@link #getModifier}.
*/
public ParameterizedModifier(Modifier positive, Modifier zero, Modifier negative) {
public AdoptingModifierStore(Modifier positive, Modifier zero, Modifier negative) {
this.positive = positive;
this.zero = zero;
this.negative = negative;
@ -36,7 +37,7 @@ public class ParameterizedModifier {
* <p>
* If this constructor is used, a plural form MUST be passed to {@link #getModifier}.
*/
public ParameterizedModifier() {
public AdoptingModifierStore() {
this.positive = null;
this.zero = null;
this.negative = null;
@ -53,7 +54,7 @@ public class ParameterizedModifier {
frozen = true;
}
public Modifier getModifier(int signum) {
public Modifier getModifierWithoutPlural(int signum) {
assert frozen;
assert mods == null;
return signum == 0 ? zero : signum < 0 ? negative : positive;
@ -66,6 +67,8 @@ public class ParameterizedModifier {
}
private static int getModIndex(int signum, StandardPlural plural) {
assert signum >= -1 && signum <= 1;
assert plural != null;
return plural.ordinal() * 3 + (signum + 1);
}
}

View File

@ -74,6 +74,28 @@ public class ConstantAffixModifier implements Modifier {
return strong;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used.
assert false;
return false;
}
@Override
public Parameters getParameters() {
return null;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
if (!(other instanceof ConstantAffixModifier)) {
return false;
}
ConstantAffixModifier _other = (ConstantAffixModifier) other;
return prefix.equals(_other.prefix) && suffix.equals(_other.suffix) && field == _other.field
&& strong == _other.strong;
}
@Override
public String toString() {
return String.format("<ConstantAffixModifier prefix:'%s' suffix:'%s'>", prefix, suffix);

View File

@ -2,6 +2,8 @@
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import java.util.Arrays;
import com.ibm.icu.text.NumberFormat.Field;
/**
@ -20,17 +22,30 @@ public class ConstantMultiFieldModifier implements Modifier {
private final boolean overwrite;
private final boolean strong;
// Parameters: used for number range formatting
private final Parameters parameters;
public ConstantMultiFieldModifier(
NumberStringBuilder prefix,
NumberStringBuilder suffix,
boolean overwrite,
boolean strong) {
this(prefix, suffix, overwrite, strong, null);
}
public ConstantMultiFieldModifier(
NumberStringBuilder prefix,
NumberStringBuilder suffix,
boolean overwrite,
boolean strong,
Parameters parameters) {
prefixChars = prefix.toCharArray();
suffixChars = suffix.toCharArray();
prefixFields = prefix.toFieldArray();
suffixFields = suffix.toFieldArray();
this.overwrite = overwrite;
this.strong = strong;
this.parameters = parameters;
}
@Override
@ -59,6 +74,40 @@ public class ConstantMultiFieldModifier implements Modifier {
return strong;
}
@Override
public boolean containsField(Field field) {
for (int i = 0; i < prefixFields.length; i++) {
if (prefixFields[i] == field) {
return true;
}
}
for (int i = 0; i < suffixFields.length; i++) {
if (suffixFields[i] == field) {
return true;
}
}
return false;
}
@Override
public Parameters getParameters() {
return parameters;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
if (!(other instanceof ConstantMultiFieldModifier)) {
return false;
}
ConstantMultiFieldModifier _other = (ConstantMultiFieldModifier) other;
if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) {
return true;
}
return Arrays.equals(prefixChars, _other.prefixChars) && Arrays.equals(prefixFields, _other.prefixFields)
&& Arrays.equals(suffixChars, _other.suffixChars) && Arrays.equals(suffixFields, _other.suffixFields)
&& overwrite == _other.overwrite && strong == _other.strong;
}
@Override
public String toString() {
NumberStringBuilder temp = new NumberStringBuilder();

View File

@ -1014,6 +1014,46 @@ public abstract class DecimalQuantity_AbstractBCD implements DecimalQuantity {
}
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (!(other instanceof DecimalQuantity_AbstractBCD)) {
return false;
}
DecimalQuantity_AbstractBCD _other = (DecimalQuantity_AbstractBCD) other;
boolean basicEquals =
scale == _other.scale
&& precision == _other.precision
&& flags == _other.flags
&& lOptPos == _other.lOptPos
&& lReqPos == _other.lReqPos
&& rReqPos == _other.rReqPos
&& rOptPos == _other.rOptPos
&& isApproximate == _other.isApproximate;
if (!basicEquals) {
return false;
}
if (precision == 0) {
return true;
} else if (isApproximate) {
return origDouble == _other.origDouble && origDelta == _other.origDelta;
} else {
for (int m = getUpperDisplayMagnitude(); m >= getLowerDisplayMagnitude(); m--) {
if (getDigit(m) != _other.getDigit(m)) {
return false;
}
}
return true;
}
}
/**
* Returns a single digit from the BCD list. No internal state is changed by calling this method.
*

View File

@ -21,7 +21,7 @@ import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
public class LongNameHandler implements MicroPropsGenerator {
public class LongNameHandler implements MicroPropsGenerator, ModifierStore {
private static final int DNAM_INDEX = StandardPlural.COUNT;
private static final int PER_INDEX = StandardPlural.COUNT + 1;
@ -175,10 +175,11 @@ public class LongNameHandler implements MicroPropsGenerator {
String[] simpleFormats = new String[ARRAY_LENGTH];
getCurrencyLongNameData(locale, currency, simpleFormats);
// TODO(ICU4J): Reduce the number of object creations here?
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
StandardPlural.class);
simpleFormatsToModifiers(simpleFormats, null, modifiers);
return new LongNameHandler(modifiers, rules, parent);
LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
result.simpleFormatsToModifiers(simpleFormats, null);
return result;
}
public static LongNameHandler forMeasureUnit(
@ -203,10 +204,11 @@ public class LongNameHandler implements MicroPropsGenerator {
getMeasureData(locale, unit, width, simpleFormats);
// TODO: What field to use for units?
// TODO(ICU4J): Reduce the number of object creations here?
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
StandardPlural.class);
simpleFormatsToModifiers(simpleFormats, null, modifiers);
return new LongNameHandler(modifiers, rules, parent);
LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
result.simpleFormatsToModifiers(simpleFormats, null);
return result;
}
private static LongNameHandler forCompoundUnit(
@ -238,29 +240,32 @@ public class LongNameHandler implements MicroPropsGenerator {
perUnitFormat = SimpleFormatterImpl.formatCompiledPattern(compiled, "{0}", secondaryString);
}
// TODO: What field to use for units?
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<StandardPlural, SimpleModifier>(
Map<StandardPlural, SimpleModifier> modifiers = new EnumMap<>(
StandardPlural.class);
multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null, modifiers);
return new LongNameHandler(modifiers, rules, parent);
LongNameHandler result = new LongNameHandler(modifiers, rules, parent);
result.multiSimpleFormatsToModifiers(primaryData, perUnitFormat, null);
return result;
}
private static void simpleFormatsToModifiers(
private void simpleFormatsToModifiers(
String[] simpleFormats,
NumberFormat.Field field,
Map<StandardPlural, SimpleModifier> output) {
NumberFormat.Field field) {
StringBuilder sb = new StringBuilder();
for (StandardPlural plural : StandardPlural.VALUES) {
String simpleFormat = getWithPlural(simpleFormats, plural);
String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 0, 1);
output.put(plural, new SimpleModifier(compiled, field, false));
Modifier.Parameters parameters = new Modifier.Parameters();
parameters.obj = this;
parameters.signum = 0;
parameters.plural = plural;
modifiers.put(plural, new SimpleModifier(compiled, field, false, parameters));
}
}
private static void multiSimpleFormatsToModifiers(
private void multiSimpleFormatsToModifiers(
String[] leadFormats,
String trailFormat,
NumberFormat.Field field,
Map<StandardPlural, SimpleModifier> output) {
NumberFormat.Field field) {
StringBuilder sb = new StringBuilder();
String trailCompiled = SimpleFormatterImpl.compileToStringMinMaxArguments(trailFormat, sb, 1, 1);
for (StandardPlural plural : StandardPlural.VALUES) {
@ -268,7 +273,11 @@ public class LongNameHandler implements MicroPropsGenerator {
String compoundFormat = SimpleFormatterImpl.formatCompiledPattern(trailCompiled, leadFormat);
String compoundCompiled = SimpleFormatterImpl
.compileToStringMinMaxArguments(compoundFormat, sb, 0, 1);
output.put(plural, new SimpleModifier(compoundCompiled, field, false));
Modifier.Parameters parameters = new Modifier.Parameters();
parameters.obj = this;
parameters.signum = 0;
parameters.plural = plural;
modifiers.put(plural, new SimpleModifier(compoundCompiled, field, false, parameters));
}
}
@ -281,4 +290,9 @@ public class LongNameHandler implements MicroPropsGenerator {
micros.modOuter = modifiers.get(copy.getStandardPlural(rules));
return micros;
}
@Override
public Modifier getModifier(int signum, StandardPlural plural) {
return modifiers.get(plural);
}
}

View File

@ -2,6 +2,9 @@
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.text.NumberFormat.Field;
/**
* A Modifier is an object that can be passed through the formatting pipeline until it is finally applied
* to the string builder. A Modifier usually contains a prefix and a suffix that are applied, but it
@ -48,4 +51,30 @@ public interface Modifier {
* @return Whether the modifier is strong.
*/
public boolean isStrong();
/**
* Whether the modifier contains at least one occurrence of the given field.
*/
public boolean containsField(Field currency);
/**
* A fill-in for getParameters(). obj will always be set; if non-null, the other
* two fields are also safe to read.
*/
public static class Parameters {
public ModifierStore obj;
public int signum;
public StandardPlural plural;
}
/**
* Gets a set of "parameters" for this Modifier.
*/
public Parameters getParameters();
/**
* Returns whether this Modifier is *semantically equivalent* to the other Modifier;
* in many cases, this is the same as equal, but parameters should be ignored.
*/
public boolean semanticallyEquivalent(Modifier other);
}

View File

@ -0,0 +1,18 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.StandardPlural;
/**
* This is *not* a modifier; rather, it is an object that can return modifiers
* based on given parameters.
*
* @author sffc
*/
public interface ModifierStore {
/**
* Returns a Modifier with the given parameters (best-effort).
*/
Modifier getModifier(int signum, StandardPlural plural);
}

View File

@ -7,6 +7,7 @@ import com.ibm.icu.impl.number.AffixUtils.SymbolProvider;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
@ -165,7 +166,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
NumberStringBuilder b = new NumberStringBuilder();
if (needsPlurals()) {
// Slower path when we require the plural keyword.
ParameterizedModifier pm = new ParameterizedModifier();
AdoptingModifierStore pm = new AdoptingModifierStore();
for (StandardPlural plural : StandardPlural.VALUES) {
setNumberProperties(1, plural);
pm.setModifier(1, plural, createConstantModifier(a, b));
@ -184,7 +185,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
Modifier zero = createConstantModifier(a, b);
setNumberProperties(-1, null);
Modifier negative = createConstantModifier(a, b);
ParameterizedModifier pm = new ParameterizedModifier(positive, zero, negative);
AdoptingModifierStore pm = new AdoptingModifierStore(positive, zero, negative);
return new ImmutablePatternModifier(pm, null, parent);
}
}
@ -213,12 +214,12 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
}
public static class ImmutablePatternModifier implements MicroPropsGenerator {
final ParameterizedModifier pm;
final AdoptingModifierStore pm;
final PluralRules rules;
final MicroPropsGenerator parent;
ImmutablePatternModifier(
ParameterizedModifier pm,
AdoptingModifierStore pm,
PluralRules rules,
MicroPropsGenerator parent) {
this.pm = pm;
@ -235,7 +236,7 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
public void applyToMicros(MicroProps micros, DecimalQuantity quantity) {
if (rules == null) {
micros.modMiddle = pm.getModifier(quantity.signum());
micros.modMiddle = pm.getModifierWithoutPlural(quantity.signum());
} else {
// TODO: Fix this. Avoid the copy.
DecimalQuantity copy = quantity.createCopy();
@ -319,6 +320,27 @@ public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPr
return isStrong;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public Parameters getParameters() {
// This method is not currently used.
assert false;
return null;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
private int insertPrefix(NumberStringBuilder sb, int position) {
prepareAffix(true);
int length = AffixUtils.unescape(currentAffix, sb, position, this);

View File

@ -3,7 +3,9 @@
package com.ibm.icu.impl.number;
import com.ibm.icu.impl.SimpleFormatterImpl;
import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.util.ICUException;
/**
* The second primary implementation of {@link Modifier}, this one consuming a
@ -17,15 +19,24 @@ public class SimpleModifier implements Modifier {
private final int suffixOffset;
private final int suffixLength;
// Parameters: used for number range formatting
private final Parameters parameters;
/** TODO: This is copied from SimpleFormatterImpl. */
private static final int ARG_NUM_LIMIT = 0x100;
/** Creates a modifier that uses the SimpleFormatter string formats. */
public SimpleModifier(String compiledPattern, Field field, boolean strong) {
this(compiledPattern, field, strong, null);
}
/** Creates a modifier that uses the SimpleFormatter string formats. */
public SimpleModifier(String compiledPattern, Field field, boolean strong, Parameters parameters) {
assert compiledPattern != null;
this.compiledPattern = compiledPattern;
this.field = field;
this.strong = strong;
this.parameters = parameters;
int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern);
if (argLimit == 0) {
@ -80,6 +91,30 @@ public class SimpleModifier implements Modifier {
return strong;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used.
assert false;
return false;
}
@Override
public Parameters getParameters() {
return parameters;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
if (!(other instanceof SimpleModifier)) {
return false;
}
SimpleModifier _other = (SimpleModifier) other;
if (parameters != null && _other.parameters != null && parameters.obj == _other.parameters.obj) {
return true;
}
return compiledPattern.equals(_other.compiledPattern) && field == _other.field && strong == _other.strong;
}
/**
* TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because
* DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not
@ -123,4 +158,66 @@ public class SimpleModifier implements Modifier {
return prefixLength + suffixLength;
}
}
/**
* TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code.
* I put it here so that the SimpleFormatter uses in NumberStringBuilder are near each other.
*
* <p>
* Applies the compiled two-argument pattern to the NumberStringBuilder.
*
* <p>
* This method is optimized for the case where the prefix and suffix are often empty, such as
* in the range pattern like "{0}-{1}".
*/
public static void formatTwoArgPattern(String compiledPattern, NumberStringBuilder result, int index, PrefixInfixSuffixLengthHelper h,
Field field) {
int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern);
if (argLimit != 2) {
throw new ICUException();
}
int offset = 1; // offset into compiledPattern
int length = 0; // chars added to result
int prefixLength = compiledPattern.charAt(offset);
offset++;
if (prefixLength < ARG_NUM_LIMIT) {
// No prefix
prefixLength = 0;
} else {
prefixLength -= ARG_NUM_LIMIT;
result.insert(index + length, compiledPattern, offset, offset + prefixLength, field);
offset += prefixLength;
length += prefixLength;
offset++;
}
int infixLength = compiledPattern.charAt(offset);
offset++;
if (infixLength < ARG_NUM_LIMIT) {
// No infix
infixLength = 0;
} else {
infixLength -= ARG_NUM_LIMIT;
result.insert(index + length, compiledPattern, offset, offset + infixLength, field);
offset += infixLength;
length += infixLength;
offset++;
}
int suffixLength;
if (offset == compiledPattern.length()) {
// No suffix
suffixLength = 0;
} else {
suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
offset++;
result.insert(index + length, compiledPattern, offset, offset + suffixLength, field);
length += suffixLength;
}
h.lengthPrefix = prefixLength;
h.lengthInfix = infixLength;
h.lengthSuffix = suffixLength;
}
}

View File

@ -0,0 +1,30 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.range;
/**
* A small, mutable internal helper class for keeping track of offsets on range patterns.
*/
public class PrefixInfixSuffixLengthHelper {
public int lengthPrefix = 0;
public int length1 = 0;
public int lengthInfix = 0;
public int length2 = 0;
public int lengthSuffix = 0;
public int index0() {
return lengthPrefix;
}
public int index1() {
return lengthPrefix + length1;
}
public int index2() {
return lengthPrefix + length1 + lengthInfix;
}
public int index3() {
return lengthPrefix + length1 + lengthInfix + length2;
}
}

View File

@ -0,0 +1,48 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.range;
import java.util.Objects;
import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
import com.ibm.icu.number.UnlocalizedNumberFormatter;
import com.ibm.icu.util.ULocale;
/**
* @author sffc
*
*/
public class RangeMacroProps {
public UnlocalizedNumberFormatter formatter1;
public UnlocalizedNumberFormatter formatter2;
public int sameFormatters = -1; // -1 for unset, 0 for false, 1 for true
public RangeCollapse collapse;
public RangeIdentityFallback identityFallback;
public ULocale loc;
@Override
public int hashCode() {
return Objects.hash(formatter1,
formatter2,
collapse,
identityFallback,
loc);
}
@Override
public boolean equals(Object _other) {
if (_other == null)
return false;
if (this == _other)
return true;
if (!(_other instanceof RangeMacroProps))
return false;
RangeMacroProps other = (RangeMacroProps) _other;
return Objects.equals(formatter1, other.formatter1)
&& Objects.equals(formatter2, other.formatter2)
&& Objects.equals(collapse, other.collapse)
&& Objects.equals(identityFallback, other.identityFallback)
&& Objects.equals(loc, other.loc);
}
}

View File

@ -0,0 +1,104 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.impl.number.range;
import java.util.MissingResourceException;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* @author sffc
*
*/
public class StandardPluralRanges {
StandardPlural[] flatTriples;
int numTriples = 0;
////////////////////
private static final class PluralRangesDataSink extends UResource.Sink {
StandardPluralRanges output;
PluralRangesDataSink(StandardPluralRanges output) {
this.output = output;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
UResource.Array entriesArray = value.getArray();
output.setCapacity(entriesArray.getSize());
for (int i = 0; entriesArray.getValue(i, value); ++i) {
UResource.Array pluralFormsArray = value.getArray();
pluralFormsArray.getValue(0, value);
StandardPlural first = StandardPlural.fromString(value.getString());
pluralFormsArray.getValue(1, value);
StandardPlural second = StandardPlural.fromString(value.getString());
pluralFormsArray.getValue(2, value);
StandardPlural result = StandardPlural.fromString(value.getString());
output.addPluralRange(first, second, result);
}
}
}
private static void getPluralRangesData(
ULocale locale,
StandardPluralRanges out) {
StringBuilder sb = new StringBuilder();
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "pluralRanges");
sb.append("locales/");
sb.append(locale.getLanguage());
String key = sb.toString();
String set;
try {
set = resource.getStringWithFallback(key);
} catch (MissingResourceException e) {
// Not all languages are covered: fail gracefully
return;
}
sb.setLength(0);
sb.append("rules/");
sb.append(set);
key = sb.toString();
PluralRangesDataSink sink = new PluralRangesDataSink(out);
resource.getAllItemsWithFallback(key, sink);
}
////////////////////
public StandardPluralRanges(ULocale locale) {
getPluralRangesData(locale, this);
}
/** Used for data loading. */
private void addPluralRange(StandardPlural first, StandardPlural second, StandardPlural result) {
flatTriples[3 * numTriples] = first;
flatTriples[3 * numTriples + 1] = second;
flatTriples[3 * numTriples + 2] = result;
numTriples++;
}
/** Used for data loading. */
private void setCapacity(int length) {
flatTriples = new StandardPlural[length*3];
}
public StandardPlural resolve(StandardPlural first, StandardPlural second) {
for (int i = 0; i < numTriples; i++) {
if (first == flatTriples[3 * i] && second == flatTriples[3 * i + 1]) {
return flatTriples[3 * i + 2];
}
}
// Default fallback
return StandardPlural.OTHER;
}
}

View File

@ -70,9 +70,9 @@ public class FormattedNumber {
}
/**
* Determine the start and end indices of the first occurrence of the given <em>field</em> in the
* output string. This allows you to determine the locations of the integer part, fraction part, and
* sign.
* Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given
* <em>field</em> in the output string. This allows you to determine the locations of, for example,
* the integer part, fraction part, or symbols.
*
* <p>
* If multiple different field attributes are needed, this method can be called repeatedly, or if

View File

@ -0,0 +1,209 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.util.Arrays;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
import com.ibm.icu.util.ICUUncheckedIOException;
/**
* The result of a number range formatting operation. This class allows the result to be exported in several data types,
* including a String, an AttributedCharacterIterator, and a BigDecimal.
*
* @author sffc
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public class FormattedNumberRange {
final NumberStringBuilder string;
final DecimalQuantity quantity1;
final DecimalQuantity quantity2;
final RangeIdentityResult identityResult;
FormattedNumberRange(NumberStringBuilder string, DecimalQuantity quantity1, DecimalQuantity quantity2,
RangeIdentityResult identityResult) {
this.string = string;
this.quantity1 = quantity1;
this.quantity2 = quantity2;
this.identityResult = identityResult;
}
/**
* Creates a String representation of the the formatted number range.
*
* @return a String containing the localized number range.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
@Override
public String toString() {
return string.toString();
}
/**
* Append the formatted number range to an Appendable, such as a StringBuilder. This may be slightly more efficient
* than creating a String.
*
* <p>
* If an IOException occurs when appending to the Appendable, an unchecked {@link ICUUncheckedIOException} is thrown
* instead.
*
* @param appendable
* The Appendable to which to append the formatted number range string.
* @return The same Appendable, for chaining.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see Appendable
* @see NumberRangeFormatter
*/
public <A extends Appendable> A appendTo(A appendable) {
try {
appendable.append(string);
} catch (IOException e) {
// Throw as an unchecked exception to avoid users needing try/catch
throw new ICUUncheckedIOException(e);
}
return appendable;
}
/**
* Determines the start (inclusive) and end (exclusive) indices of the next occurrence of the given
* <em>field</em> in the output string. This allows you to determine the locations of, for example,
* the integer part, fraction part, or symbols.
* <p>
* If both sides of the range have the same field, the field will occur twice, once before the range separator and
* once after the range separator, if applicable.
* <p>
* If a field occurs just once, calling this method will find that occurrence and return it. If a field occurs
* multiple times, this method may be called repeatedly with the following pattern:
*
* <pre>
* FieldPosition fpos = new FieldPosition(NumberFormat.Field.INTEGER);
* while (formattedNumberRange.nextFieldPosition(fpos, status)) {
* // do something with fpos.
* }
* </pre>
* <p>
* This method is useful if you know which field to query. If you want all available field position information, use
* {@link #toCharacterIterator()}.
*
* @param fieldPosition
* Input+output variable. See {@link FormattedNumber#nextFieldPosition(FieldPosition)}.
* @return true if a new occurrence of the field was found; false otherwise.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see com.ibm.icu.text.NumberFormat.Field
* @see NumberRangeFormatter
*/
public boolean nextFieldPosition(FieldPosition fieldPosition) {
return string.nextFieldPosition(fieldPosition);
}
/**
* Export the formatted number range as an AttributedCharacterIterator. This allows you to determine which
* characters in the output string correspond to which <em>fields</em>, such as the integer part, fraction part, and
* sign.
* <p>
* If information on only one field is needed, use {@link #nextFieldPosition(FieldPosition)} instead.
*
* @return An AttributedCharacterIterator, containing information on the field attributes of the number range
* string.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see com.ibm.icu.text.NumberFormat.Field
* @see AttributedCharacterIterator
* @see NumberRangeFormatter
*/
public AttributedCharacterIterator toCharacterIterator() {
return string.toCharacterIterator();
}
/**
* Export the first formatted number as a BigDecimal. This endpoint is useful for obtaining the exact number being
* printed after scaling and rounding have been applied by the number range formatting pipeline.
*
* @return A BigDecimal representation of the first formatted number.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
* @see #getSecondBigDecimal
*/
public BigDecimal getFirstBigDecimal() {
return quantity1.toBigDecimal();
}
/**
* Export the second formatted number as a BigDecimal. This endpoint is useful for obtaining the exact number being
* printed after scaling and rounding have been applied by the number range formatting pipeline.
*
* @return A BigDecimal representation of the second formatted number.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
* @see #getFirstBigDecimal
*/
public BigDecimal getSecondBigDecimal() {
return quantity2.toBigDecimal();
}
/**
* Returns whether the pair of numbers was successfully formatted as a range or whether an identity fallback was
* used. For example, if the first and second number were the same either before or after rounding occurred, an
* identity fallback was used.
*
* @return A RangeIdentityType indicating the resulting identity situation in the formatted number range.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
* @see NumberRangeFormatter.RangeIdentityFallback
*/
public RangeIdentityResult getIdentityResult() {
return identityResult;
}
/**
* {@inheritDoc}
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
*/
@Override
public int hashCode() {
// NumberStringBuilder and BigDecimal are mutable, so we can't call
// #equals() or #hashCode() on them directly.
return Arrays.hashCode(string.toCharArray()) ^ Arrays.hashCode(string.toFieldArray())
^ quantity1.toBigDecimal().hashCode() ^ quantity2.toBigDecimal().hashCode();
}
/**
* {@inheritDoc}
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
*/
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (other == null)
return false;
if (!(other instanceof FormattedNumberRange))
return false;
// NumberStringBuilder and BigDecimal are mutable, so we can't call
// #equals() or #hashCode() on them directly.
FormattedNumberRange _other = (FormattedNumberRange) other;
return Arrays.equals(string.toCharArray(), _other.string.toCharArray())
&& Arrays.equals(string.toFieldArray(), _other.string.toFieldArray())
&& quantity1.toBigDecimal().equals(_other.quantity1.toBigDecimal())
&& quantity2.toBigDecimal().equals(_other.quantity2.toBigDecimal());
}
}

View File

@ -152,9 +152,9 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
public FormattedNumber format(DecimalQuantity fq) {
NumberStringBuilder string = new NumberStringBuilder();
if (computeCompiled()) {
compiled.apply(fq, string);
compiled.format(fq, string);
} else {
NumberFormatterImpl.applyStatic(resolve(), fq, string);
NumberFormatterImpl.formatStatic(resolve(), fq, string);
}
return new FormattedNumber(string, fq);
}
@ -190,7 +190,7 @@ public class LocalizedNumberFormatter extends NumberFormatterSettings<LocalizedN
// Further benchmarking is required.
long currentCount = callCount.incrementAndGet(this);
if (currentCount == macros.threshold.longValue()) {
compiled = NumberFormatterImpl.fromMacros(macros);
compiled = new NumberFormatterImpl(macros);
return true;
} else if (compiled != null) {
return true;

View File

@ -0,0 +1,98 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
/**
* A NumberRangeFormatter that has a locale associated with it; this means .formatRange() methods are available.
*
* @author sffc
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public class LocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<LocalizedNumberRangeFormatter> {
private volatile NumberRangeFormatterImpl fImpl;
LocalizedNumberRangeFormatter(NumberRangeFormatterSettings<?> parent, int key, Object value) {
super(parent, key, value);
}
/**
* Format the given integers to a string using the settings specified in the NumberRangeFormatter fluent setting
* chain.
*
* @param first
* The first number in the range, usually to the left in LTR locales.
* @param second
* The second number in the range, usually to the right in LTR locales.
* @return A FormattedNumberRange object; call .toString() to get the string.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public FormattedNumberRange formatRange(int first, int second) {
DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first);
DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second);
return formatImpl(dq1, dq2, first == second);
}
/**
* Format the given doubles to a string using the settings specified in the NumberRangeFormatter fluent setting
* chain.
*
* @param first
* The first number in the range, usually to the left in LTR locales.
* @param second
* The second number in the range, usually to the right in LTR locales.
* @return A FormattedNumberRange object; call .toString() to get the string.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public FormattedNumberRange formatRange(double first, double second) {
DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first);
DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second);
// Note: double equality could be changed to epsilon equality later if there is demand.
// The epsilon should be set via an API method.
return formatImpl(dq1, dq2, first == second);
}
/**
* Format the given Numbers to a string using the settings specified in the NumberRangeFormatter fluent setting
* chain.
*
* @param first
* The first number in the range, usually to the left in LTR locales.
* @param second
* The second number in the range, usually to the right in LTR locales.
* @return A FormattedNumberRange object; call .toString() to get the string.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public FormattedNumberRange formatRange(Number first, Number second) {
if (first == null || second == null) {
throw new IllegalArgumentException("Cannot format null values in range");
}
DecimalQuantity dq1 = new DecimalQuantity_DualStorageBCD(first);
DecimalQuantity dq2 = new DecimalQuantity_DualStorageBCD(second);
return formatImpl(dq1, dq2, first.equals(second));
}
FormattedNumberRange formatImpl(DecimalQuantity first, DecimalQuantity second, boolean equalBeforeRounding) {
if (fImpl == null) {
fImpl = new NumberRangeFormatterImpl(resolve());
}
return fImpl.format(first, second, equalBeforeRounding);
}
@Override
LocalizedNumberRangeFormatter create(int key, Object value) {
return new LocalizedNumberRangeFormatter(this, key, value);
}
}

View File

@ -42,21 +42,21 @@ import com.ibm.icu.util.MeasureUnit;
class NumberFormatterImpl {
/** Builds a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly. */
public static NumberFormatterImpl fromMacros(MacroProps macros) {
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true);
return new NumberFormatterImpl(microPropsGenerator);
public NumberFormatterImpl(MacroProps macros) {
this(macrosToMicroGenerator(macros, true));
}
/**
* Builds and evaluates an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
*/
public static void applyStatic(
public static int formatStatic(
MacroProps macros,
DecimalQuantity inValue,
NumberStringBuilder outString) {
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
MicroProps micros = microPropsGenerator.processQuantity(inValue);
microsToString(micros, inValue, outString);
MicroProps micros = preProcessUnsafe(macros, inValue);
int length = writeNumber(micros, inValue, outString, 0);
length += writeAffixes(micros, outString, 0, length);
return length;
}
/**
@ -82,9 +82,40 @@ class NumberFormatterImpl {
this.microPropsGenerator = microPropsGenerator;
}
public void apply(DecimalQuantity inValue, NumberStringBuilder outString) {
/**
* Evaluates the "safe" MicroPropsGenerator created by "fromMacros".
*/
public int format(DecimalQuantity inValue, NumberStringBuilder outString) {
MicroProps micros = preProcess(inValue);
int length = writeNumber(micros, inValue, outString, 0);
length += writeAffixes(micros, outString, 0, length);
return length;
}
/**
* Like format(), but saves the result into an output MicroProps without additional processing.
*/
public MicroProps preProcess(DecimalQuantity inValue) {
MicroProps micros = microPropsGenerator.processQuantity(inValue);
microsToString(micros, inValue, outString);
micros.rounder.apply(inValue);
if (micros.integerWidth.maxInt == -1) {
inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
} else {
inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
}
return micros;
}
private static MicroProps preProcessUnsafe(MacroProps macros, DecimalQuantity inValue) {
MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
MicroProps micros = microPropsGenerator.processQuantity(inValue);
micros.rounder.apply(inValue);
if (micros.integerWidth.maxInt == -1) {
inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
} else {
inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
}
return micros;
}
public int getPrefixSuffix(byte signum, StandardPlural plural, NumberStringBuilder output) {
@ -350,64 +381,55 @@ class NumberFormatterImpl {
//////////
/**
* Synthesizes the output string from a MicroProps and DecimalQuantity.
*
* @param micros
* The MicroProps after the quantity has been consumed. Will not be mutated.
* @param quantity
* The DecimalQuantity to be rendered. May be mutated.
* @param string
* The output string. Will be mutated.
* Adds the affixes. Intended to be called immediately after formatNumber.
*/
private static void microsToString(
public static int writeAffixes(
MicroProps micros,
DecimalQuantity quantity,
NumberStringBuilder string) {
micros.rounder.apply(quantity);
if (micros.integerWidth.maxInt == -1) {
quantity.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
} else {
quantity.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
}
int length = writeNumber(micros, quantity, string);
// NOTE: When range formatting is added, these modifiers can bubble up.
// For now, apply them all here at once.
NumberStringBuilder string,
int start,
int end) {
// Always apply the inner modifier (which is "strong").
length += micros.modInner.apply(string, 0, length);
int length = micros.modInner.apply(string, start, end);
if (micros.padding.isValid()) {
micros.padding.padAndApply(micros.modMiddle, micros.modOuter, string, 0, length);
micros.padding.padAndApply(micros.modMiddle, micros.modOuter, string, start, end + length);
} else {
length += micros.modMiddle.apply(string, 0, length);
length += micros.modOuter.apply(string, 0, length);
length += micros.modMiddle.apply(string, start, end + length);
length += micros.modOuter.apply(string, start, end + length);
}
return length;
}
private static int writeNumber(
/**
* Synthesizes the output string from a MicroProps and DecimalQuantity.
* This method formats only the main number, not affixes.
*/
public static int writeNumber(
MicroProps micros,
DecimalQuantity quantity,
NumberStringBuilder string) {
NumberStringBuilder string,
int index) {
int length = 0;
if (quantity.isInfinite()) {
length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
length += string.insert(length + index, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
} else if (quantity.isNaN()) {
length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
length += string.insert(length + index, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
} else {
// Add the integer digits
length += writeIntegerDigits(micros, quantity, string);
length += writeIntegerDigits(micros, quantity, string, length + index);
// Add the decimal point
if (quantity.getLowerDisplayMagnitude() < 0
|| micros.decimal == DecimalSeparatorDisplay.ALWAYS) {
length += string.insert(length,
length += string.insert(length + index,
micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString()
: micros.symbols.getDecimalSeparatorString(),
NumberFormat.Field.DECIMAL_SEPARATOR);
}
// Add the fraction digits
length += writeFractionDigits(micros, quantity, string);
length += writeFractionDigits(micros, quantity, string, length + index);
}
return length;
@ -416,13 +438,14 @@ class NumberFormatterImpl {
private static int writeIntegerDigits(
MicroProps micros,
DecimalQuantity quantity,
NumberStringBuilder string) {
NumberStringBuilder string,
int index) {
int length = 0;
int integerCount = quantity.getUpperDisplayMagnitude() + 1;
for (int i = 0; i < integerCount; i++) {
// Add grouping separator
if (micros.grouping.groupAtPosition(i, quantity)) {
length += string.insert(0,
length += string.insert(index,
micros.useCurrency ? micros.symbols.getMonetaryGroupingSeparatorString()
: micros.symbols.getGroupingSeparatorString(),
NumberFormat.Field.GROUPING_SEPARATOR);
@ -431,11 +454,11 @@ class NumberFormatterImpl {
// Get and append the next digit value
byte nextDigit = quantity.getDigit(i);
if (micros.symbols.getCodePointZero() != -1) {
length += string.insertCodePoint(0,
length += string.insertCodePoint(index,
micros.symbols.getCodePointZero() + nextDigit,
NumberFormat.Field.INTEGER);
} else {
length += string.insert(0,
length += string.insert(index,
micros.symbols.getDigitStringsLocal()[nextDigit],
NumberFormat.Field.INTEGER);
}
@ -446,17 +469,18 @@ class NumberFormatterImpl {
private static int writeFractionDigits(
MicroProps micros,
DecimalQuantity quantity,
NumberStringBuilder string) {
NumberStringBuilder string,
int index) {
int length = 0;
int fractionCount = -quantity.getLowerDisplayMagnitude();
for (int i = 0; i < fractionCount; i++) {
// Get and append the next digit value
byte nextDigit = quantity.getDigit(-i - 1);
if (micros.symbols.getCodePointZero() != -1) {
length += string.appendCodePoint(micros.symbols.getCodePointZero() + nextDigit,
length += string.insertCodePoint(length + index, micros.symbols.getCodePointZero() + nextDigit,
NumberFormat.Field.FRACTION);
} else {
length += string.append(micros.symbols.getDigitStringsLocal()[nextDigit],
length += string.insert(length + index, micros.symbols.getDigitStringsLocal()[nextDigit],
NumberFormat.Field.FRACTION);
}
}

View File

@ -47,10 +47,10 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
static final int KEY_PER_UNIT = 15;
static final int KEY_MAX = 16;
final NumberFormatterSettings<?> parent;
final int key;
final Object value;
volatile MacroProps resolvedMacros;
private final NumberFormatterSettings<?> parent;
private final int key;
private final Object value;
private volatile MacroProps resolvedMacros;
NumberFormatterSettings(NumberFormatterSettings<?> parent, int key, Object value) {
this.parent = parent;

View File

@ -0,0 +1,209 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import java.util.Locale;
import com.ibm.icu.util.ULocale;
/**
* The main entrypoint to the formatting of ranges of numbers, including currencies and other units of measurement.
* <p>
* Usage example:
* <pre>
* NumberRangeFormatter.with()
* .identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)
* .numberFormatterFirst(NumberFormatter.with().unit(MeasureUnit.METER))
* .numberFormatterSecond(NumberFormatter.with().unit(MeasureUnit.KILOMETER))
* .locale(ULocale.UK)
* .formatRange(750, 1.2)
* .toString();
* // => "750 m - 1.2 km"
* </pre>
* <p>
* Like NumberFormatter, NumberRangeFormatter instances are immutable and thread-safe. This API is based on the
* <em>fluent</em> design pattern popularized by libraries such as Google's Guava.
*
* @author sffc
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberFormatter
*/
public abstract class NumberRangeFormatter {
/**
* Defines how to merge fields that are identical across the range sign.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public enum RangeCollapse {
/**
* Use locale data and heuristics to determine how much of the string to collapse. Could end up collapsing none,
* some, or all repeated pieces in a locale-sensitive way.
* <p>
* The heuristics used for this option are subject to change over time.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
AUTO,
/**
* Do not collapse any part of the number. Example: "3.2 thousand kilograms 5.3 thousand kilograms"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
NONE,
/**
* Collapse the unit part of the number, but not the notation, if present. Example: "3.2 thousand 5.3 thousand
* kilograms"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
UNIT,
/**
* Collapse any field that is equal across the range sign. May introduce ambiguity on the magnitude of the
* number. Example: "3.2 5.3 thousand kilograms"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
ALL
}
/**
* Defines the behavior when the two numbers in the range are identical after rounding. To programmatically detect
* when the identity fallback is used, compare the lower and upper BigDecimals via FormattedNumber.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public static enum RangeIdentityFallback {
/**
* Show the number as a single value rather than a range. Example: "$5"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
SINGLE_VALUE,
/**
* Show the number using a locale-sensitive approximation pattern. If the numbers were the same before rounding,
* show the single value. Example: "~$5" or "$5"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
APPROXIMATELY_OR_SINGLE_VALUE,
/**
* Show the number using a locale-sensitive approximation pattern. Use the range pattern always, even if the
* inputs are the same. Example: "~$5"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
APPROXIMATELY,
/**
* Show the number as the range of two equal values. Use the range pattern always, even if the inputs are the
* same. Example (with RangeCollapse.NONE): "$5 $5"
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
RANGE
}
/**
* Used in the result class FormattedNumberRange to indicate to the user whether the numbers formatted in the range
* were equal or not, and whether or not the identity fallback was applied.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public static enum RangeIdentityResult {
/**
* Used to indicate that the two numbers in the range were equal, even before any rounding rules were applied.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
EQUAL_BEFORE_ROUNDING,
/**
* Used to indicate that the two numbers in the range were equal, but only after rounding rules were applied.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
EQUAL_AFTER_ROUNDING,
/**
* Used to indicate that the two numbers in the range were not equal, even after rounding rules were applied.
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
NOT_EQUAL
}
private static final UnlocalizedNumberRangeFormatter BASE = new UnlocalizedNumberRangeFormatter();
/**
* Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is not currently
* known at the call site.
*
* @return An {@link UnlocalizedNumberRangeFormatter}, to be used for chaining.
* @draft ICU 63
*/
public static UnlocalizedNumberRangeFormatter with() {
return BASE;
}
/**
* Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is known at the call
* site.
*
* @param locale
* The locale from which to load formats and symbols for number range formatting.
* @return A {@link LocalizedNumberRangeFormatter}, to be used for chaining.
* @draft ICU 63
*/
public static LocalizedNumberRangeFormatter withLocale(Locale locale) {
return BASE.locale(locale);
}
/**
* Call this method at the beginning of a NumberRangeFormatter fluent chain in which the locale is known at the call
* site.
*
* @param locale
* The locale from which to load formats and symbols for number range formatting.
* @return A {@link LocalizedNumberRangeFormatter}, to be used for chaining.
* @draft ICU 63
*/
public static LocalizedNumberRangeFormatter withLocale(ULocale locale) {
return BASE.locale(locale);
}
}

View File

@ -0,0 +1,370 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.PatternProps;
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.MicroProps;
import com.ibm.icu.impl.number.Modifier;
import com.ibm.icu.impl.number.NumberStringBuilder;
import com.ibm.icu.impl.number.SimpleModifier;
import com.ibm.icu.impl.number.range.PrefixInfixSuffixLengthHelper;
import com.ibm.icu.impl.number.range.RangeMacroProps;
import com.ibm.icu.impl.number.range.StandardPluralRanges;
import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityResult;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
/**
* Business logic behind NumberRangeFormatter.
*/
class NumberRangeFormatterImpl {
final NumberFormatterImpl formatterImpl1;
final NumberFormatterImpl formatterImpl2;
final boolean fSameFormatters;
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.
/* final */ String fRangePattern;
/* final */ SimpleModifier fApproximatelyModifier;
final StandardPluralRanges fPluralRanges;
////////////////////
// Helper function for 2-dimensional switch statement
int identity2d(RangeIdentityFallback a, RangeIdentityResult b) {
return a.ordinal() | (b.ordinal() << 4);
}
private static final class NumberRangeDataSink extends UResource.Sink {
String rangePattern;
String approximatelyPattern;
// For use with SimpleFormatterImpl
StringBuilder sb;
NumberRangeDataSink(StringBuilder sb) {
this.sb = sb;
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
UResource.Table miscTable = value.getTable();
for (int i = 0; miscTable.getKeyAndValue(i, key, value); ++i) {
if (key.contentEquals("range") && rangePattern == null) {
String pattern = value.getString();
rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
}
if (key.contentEquals("approximately") && approximatelyPattern == null) {
String pattern = value.getString();
approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
}
}
}
}
private static void getNumberRangeData(
ULocale locale,
String nsName,
NumberRangeFormatterImpl out) {
StringBuilder sb = new StringBuilder();
NumberRangeDataSink sink = new NumberRangeDataSink(sb);
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
sb.append("NumberElements/");
sb.append(nsName);
sb.append("/miscPatterns");
String key = sb.toString();
resource.getAllItemsWithFallback(key, sink);
// TODO: Is it necessary to manually fall back to latn, or does the data sink take care of that?
if (sink.rangePattern == null) {
sink.rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}{1}", sb, 2, 2);
}
if (sink.approximatelyPattern == null) {
sink.approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1);
}
out.fRangePattern = sink.rangePattern;
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());
fSameFormatters = macros.sameFormatters != 0;
fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO;
fIdentityFallback = macros.identityFallback != null ? macros.identityFallback
: NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY;
// TODO: As of this writing (ICU 63), there is no locale that has different number miscPatterns
// based on numbering system. Therefore, data is loaded only from latn. If this changes,
// this part of the code should be updated to load from the local numbering system.
// The numbering system could come from the one specified in the NumberFormatter passed to
// numberFormatterBoth() or similar.
// See ICU-20144
getNumberRangeData(macros.loc, "latn", this);
// TODO: Get locale from PluralRules instead?
fPluralRanges = new StandardPluralRanges(macros.loc);
}
public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) {
NumberStringBuilder string = new NumberStringBuilder();
MicroProps micros1 = formatterImpl1.preProcess(quantity1);
MicroProps micros2;
if (fSameFormatters) {
micros2 = formatterImpl1.preProcess(quantity2);
} else {
micros2 = formatterImpl2.preProcess(quantity2);
}
// If any of the affixes are different, an identity is not possible
// and we must use formatRange().
// TODO: Write this as MicroProps operator==() ?
// TODO: Avoid the redundancy of these equality operations with the
// ones in formatRange?
if (!micros1.modInner.semanticallyEquivalent(micros2.modInner)
|| !micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle)
|| !micros1.modOuter.semanticallyEquivalent(micros2.modOuter)) {
formatRange(quantity1, quantity2, string, micros1, micros2);
return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL);
}
// Check for identity
RangeIdentityResult identityResult;
if (equalBeforeRounding) {
identityResult = RangeIdentityResult.EQUAL_BEFORE_ROUNDING;
} else if (quantity1.equals(quantity2)) {
identityResult = RangeIdentityResult.EQUAL_AFTER_ROUNDING;
} else {
identityResult = RangeIdentityResult.NOT_EQUAL;
}
// Java does not let us use a constexpr like C++;
// we need to expand identity2d calls.
switch (identity2d(fIdentityFallback, identityResult)) {
case (3 | (2 << 4)): // RANGE, NOT_EQUAL
case (3 | (1 << 4)): // RANGE, EQUAL_AFTER_ROUNDING
case (3 | (0 << 4)): // RANGE, EQUAL_BEFORE_ROUNDING
case (2 | (2 << 4)): // APPROXIMATELY, NOT_EQUAL
case (1 | (2 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, NOT_EQUAL
case (0 | (2 << 4)): // SINGLE_VALUE, NOT_EQUAL
formatRange(quantity1, quantity2, string, micros1, micros2);
break;
case (2 | (1 << 4)): // APPROXIMATELY, EQUAL_AFTER_ROUNDING
case (2 | (0 << 4)): // APPROXIMATELY, EQUAL_BEFORE_ROUNDING
case (1 | (1 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_AFTER_ROUNDING
formatApproximately(quantity1, quantity2, string, micros1, micros2);
break;
case (1 | (0 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_BEFORE_ROUNDING
case (0 | (1 << 4)): // SINGLE_VALUE, EQUAL_AFTER_ROUNDING
case (0 | (0 << 4)): // SINGLE_VALUE, EQUAL_BEFORE_ROUNDING
formatSingleValue(quantity1, quantity2, string, micros1, micros2);
break;
default:
assert false;
break;
}
return new FormattedNumberRange(string, quantity1, quantity2, identityResult);
}
private void formatSingleValue(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string,
MicroProps micros1, MicroProps micros2) {
if (fSameFormatters) {
int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
NumberFormatterImpl.writeAffixes(micros1, string, 0, length);
} else {
formatRange(quantity1, quantity2, string, micros1, micros2);
}
}
private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string,
MicroProps micros1, MicroProps micros2) {
if (fSameFormatters) {
int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0);
length += NumberFormatterImpl.writeAffixes(micros1, string, 0, length);
fApproximatelyModifier.apply(string, 0, length);
} else {
formatRange(quantity1, quantity2, string, micros1, micros2);
}
}
private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, NumberStringBuilder string,
MicroProps micros1, MicroProps micros2) {
// modInner is always notation (scientific); collapsable in ALL.
// modOuter is always units; collapsable in ALL, AUTO, and UNIT.
// modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT.
// Never collapse an outer mod but not an inner mod.
boolean collapseOuter, collapseMiddle, collapseInner;
switch (fCollapse) {
case ALL:
case AUTO:
case UNIT:
{
// OUTER MODIFIER
collapseOuter = micros1.modOuter.semanticallyEquivalent(micros2.modOuter);
if (!collapseOuter) {
// Never collapse inner mods if outer mods are not collapsable
collapseMiddle = false;
collapseInner = false;
break;
}
// MIDDLE MODIFIER
collapseMiddle = micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle);
if (!collapseMiddle) {
// Never collapse inner mods if outer mods are not collapsable
collapseInner = false;
break;
}
// MIDDLE MODIFIER HEURISTICS
// (could disable collapsing of the middle modifier)
// The modifiers are equal by this point, so we can look at just one of them.
Modifier mm = micros1.modMiddle;
if (fCollapse == RangeCollapse.UNIT) {
// Only collapse if the modifier is a unit.
// TODO: Make a better way to check for a unit?
// TODO: Handle case where the modifier has both notation and unit (compact currency)?
if (!mm.containsField(NumberFormat.Field.CURRENCY) && !mm.containsField(NumberFormat.Field.PERCENT)) {
collapseMiddle = false;
}
} else if (fCollapse == RangeCollapse.AUTO) {
// Heuristic as of ICU 63: collapse only if the modifier is more than one code point.
if (mm.getCodePointCount() <= 1) {
collapseMiddle = false;
}
}
if (!collapseMiddle || fCollapse != RangeCollapse.ALL) {
collapseInner = false;
break;
}
// INNER MODIFIER
collapseInner = micros1.modInner.semanticallyEquivalent(micros2.modInner);
// All done checking for collapsability.
break;
}
default:
collapseOuter = false;
collapseMiddle = false;
collapseInner = false;
break;
}
// Java doesn't have macros, constexprs, or stack objects.
// Use a helper object instead.
PrefixInfixSuffixLengthHelper h = new PrefixInfixSuffixLengthHelper();
SimpleModifier.formatTwoArgPattern(fRangePattern, string, 0, h, null);
assert h.lengthInfix > 0;
// SPACING HEURISTIC
// Add spacing unless all modifiers are collapsed.
// TODO: add API to control this?
// TODO: Use a data-driven heuristic like currency spacing?
// TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications)
{
boolean repeatInner = !collapseInner && micros1.modInner.getCodePointCount() > 0;
boolean repeatMiddle = !collapseMiddle && micros1.modMiddle.getCodePointCount() > 0;
boolean repeatOuter = !collapseOuter && micros1.modOuter.getCodePointCount() > 0;
if (repeatInner || repeatMiddle || repeatOuter) {
// Add spacing if there is not already spacing
if (!PatternProps.isWhiteSpace(string.charAt(h.index1()))) {
h.lengthInfix += string.insertCodePoint(h.index1(), '\u0020', null);
}
if (!PatternProps.isWhiteSpace(string.charAt(h.index2() - 1))) {
h.lengthInfix += string.insertCodePoint(h.index2(), '\u0020', null);
}
}
}
h.length1 += NumberFormatterImpl.writeNumber(micros1, quantity1, string, h.index0());
h.length2 += NumberFormatterImpl.writeNumber(micros2, quantity2, string, h.index2());
// TODO: Support padding?
if (collapseInner) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner);
h.lengthInfix += mod.apply(string, h.index0(), h.index3());
} else {
h.length1 += micros1.modInner.apply(string, h.index0(), h.index1());
h.length2 += micros2.modInner.apply(string, h.index2(), h.index3());
}
if (collapseMiddle) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle);
h.lengthInfix += mod.apply(string, h.index0(), h.index3());
} else {
h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1());
h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3());
}
if (collapseOuter) {
// Note: this is actually a mix of prefix and suffix, but adding to infix length works
Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter);
h.lengthInfix += mod.apply(string, h.index0(), h.index3());
} else {
h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1());
h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3());
}
}
Modifier resolveModifierPlurals(Modifier first, Modifier second) {
Modifier.Parameters firstParameters = first.getParameters();
if (firstParameters == null) {
// No plural form; return a fallback (e.g., the first)
return first;
}
Modifier.Parameters secondParameters = second.getParameters();
if (secondParameters == null) {
// No plural form; return a fallback (e.g., the first)
return first;
}
// Get the required plural form from data
StandardPlural resultPlural = fPluralRanges.resolve(firstParameters.plural, secondParameters.plural);
// Get and return the new Modifier
assert firstParameters.obj == secondParameters.obj;
assert firstParameters.signum == secondParameters.signum;
Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural);
assert mod != null;
return mod;
}
}

View File

@ -0,0 +1,240 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import com.ibm.icu.impl.number.range.RangeMacroProps;
import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
import com.ibm.icu.util.ULocale;
/**
* An abstract base class for specifying settings related to number formatting. This class is implemented by
* {@link UnlocalizedNumberRangeFormatter} and {@link LocalizedNumberRangeFormatter}. This class is not intended for
* public subclassing.
*
* @author sffc
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public abstract class NumberRangeFormatterSettings<T extends NumberRangeFormatterSettings<?>> {
static final int KEY_MACROS = 0; // not used
static final int KEY_LOCALE = 1;
static final int KEY_FORMATTER_1 = 2;
static final int KEY_FORMATTER_2 = 3;
static final int KEY_SAME_FORMATTERS = 4;
static final int KEY_COLLAPSE = 5;
static final int KEY_IDENTITY_FALLBACK = 6;
static final int KEY_MAX = 7;
private final NumberRangeFormatterSettings<?> parent;
private final int key;
private final Object value;
private volatile RangeMacroProps resolvedMacros;
NumberRangeFormatterSettings(NumberRangeFormatterSettings<?> parent, int key, Object value) {
this.parent = parent;
this.key = key;
this.value = value;
}
/**
* Sets the NumberFormatter instance to use for the numbers in the range. The same formatter is applied to both
* sides of the range.
* <p>
* The NumberFormatter instances must not have a locale applied yet; the locale specified on the
* NumberRangeFormatter will be used.
*
* @param formatter
* The formatter to use for both numbers in the range.
* @return The fluent chain.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberFormatter
* @see NumberRangeFormatter
*/
@SuppressWarnings("unchecked")
public T numberFormatterBoth(UnlocalizedNumberFormatter formatter) {
return (T) create(KEY_SAME_FORMATTERS, true).create(KEY_FORMATTER_1, formatter);
}
/**
* Sets the NumberFormatter instance to use for the first number in the range.
* <p>
* The NumberFormatter instance must not have a locale applied yet; the locale specified on the
* NumberRangeFormatter will be used.
*
* @param formatterFirst
* The formatter to use for the first number in the range.
* @return The fluent chain.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberFormatter
* @see NumberRangeFormatter
*/
@SuppressWarnings("unchecked")
public T numberFormatterFirst(UnlocalizedNumberFormatter formatterFirst) {
return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_1, formatterFirst);
}
/**
* Sets the NumberFormatter instances to use for the second number in the range.
* <p>
* The NumberFormatter instance must not have a locale applied yet; the locale specified on the
* NumberRangeFormatter will be used.
*
* @param formatterSecond
* The formatter to use for the second number in the range.
* @return The fluent chain.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberFormatter
* @see NumberRangeFormatter
*/
@SuppressWarnings("unchecked")
public T numberFormatterSecond(UnlocalizedNumberFormatter formatterSecond) {
return (T) create(KEY_SAME_FORMATTERS, false).create(KEY_FORMATTER_2, formatterSecond);
}
/**
* Sets the aggressiveness of "collapsing" fields across the range separator. Possible values:
* <ul>
* <li>ALL: "3-5K miles"</li>
* <li>UNIT: "3K - 5K miles"</li>
* <li>NONE: "3K miles - 5K miles"</li>
* <li>AUTO: usually UNIT or NONE, depending on the locale and formatter settings</li>
* </ul>
* <p>
* The default value is AUTO.
*
* @param collapse
* The collapsing strategy to use for this range.
* @return The fluent chain.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public T collapse(RangeCollapse collapse) {
return create(KEY_COLLAPSE, collapse);
}
/**
* Sets the behavior when the two sides of the range are the same. This could happen if the same two numbers are
* passed to the formatRange function, or if different numbers are passed to the function but they become the same
* after rounding rules are applied. Possible values:
* <ul>
* <li>SINGLE_VALUE: "5 miles"</li>
* <li>APPROXIMATELY_OR_SINGLE_VALUE: "~5 miles" or "5 miles", depending on whether the number was the same before
* rounding was applied</li>
* <li>APPROXIMATELY: "~5 miles"</li>
* <li>RANGE: "5-5 miles" (with collapse=UNIT)</li>
* </ul>
* <p>
* The default value is APPROXIMATELY.
*
* @param identityFallback
* The strategy to use when formatting two numbers that end up being the same.
* @return The fluent chain.
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public T identityFallback(RangeIdentityFallback identityFallback) {
return create(KEY_IDENTITY_FALLBACK, identityFallback);
}
/* package-protected */ abstract T create(int key, Object value);
RangeMacroProps resolve() {
if (resolvedMacros != null) {
return resolvedMacros;
}
// Although the linked-list fluent storage approach requires this method,
// my benchmarks show that linked-list is still faster than a full clone
// of a MacroProps object at each step.
// TODO: Remove the reference to the parent after the macros are resolved?
RangeMacroProps macros = new RangeMacroProps();
NumberRangeFormatterSettings<?> current = this;
while (current != null) {
switch (current.key) {
case KEY_MACROS:
// ignored for now
break;
case KEY_LOCALE:
if (macros.loc == null) {
macros.loc = (ULocale) current.value;
}
break;
case KEY_FORMATTER_1:
if (macros.formatter1 == null) {
macros.formatter1 = (UnlocalizedNumberFormatter) current.value;
}
break;
case KEY_FORMATTER_2:
if (macros.formatter2 == null) {
macros.formatter2 = (UnlocalizedNumberFormatter) current.value;
}
break;
case KEY_SAME_FORMATTERS:
if (macros.sameFormatters == -1) {
macros.sameFormatters = (boolean) current.value ? 1 : 0;
}
break;
case KEY_COLLAPSE:
if (macros.collapse == null) {
macros.collapse = (RangeCollapse) current.value;
}
break;
case KEY_IDENTITY_FALLBACK:
if (macros.identityFallback == null) {
macros.identityFallback = (RangeIdentityFallback) current.value;
}
break;
default:
throw new AssertionError("Unknown key: " + current.key);
}
current = current.parent;
}
// Copy the locale into the children (see touchRangeLocales in C++)
if (macros.formatter1 != null) {
macros.formatter1.resolve().loc = macros.loc;
}
if (macros.formatter2 != null) {
macros.formatter2.resolve().loc = macros.loc;
}
resolvedMacros = macros;
return macros;
}
/**
* {@inheritDoc}
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
*/
@Override
public int hashCode() {
return resolve().hashCode();
}
/**
* {@inheritDoc}
*
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (!(other instanceof NumberRangeFormatterSettings)) {
return false;
}
return resolve().equals(((NumberRangeFormatterSettings<?>) other).resolve());
}
}

View File

@ -13,6 +13,7 @@ import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.Precision.SignificantRounderImpl;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.NumberFormat.Field;
/**
* A class that defines the scientific notation style to be used when formatting numbers in
@ -221,8 +222,10 @@ public class ScientificNotation extends Notation implements Cloneable {
@Override
public int getCodePointCount() {
// This method is not used for strong modifiers.
throw new AssertionError();
// NOTE: This method is only called one place, NumberRangeFormatterImpl.
// The call site only cares about != 0 and != 1.
// Return a very large value so that if this method is used elsewhere, we should notice.
return 999;
}
@Override
@ -231,6 +234,27 @@ public class ScientificNotation extends Notation implements Cloneable {
return true;
}
@Override
public boolean containsField(Field field) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public Parameters getParameters() {
// This method is not currently used.
assert false;
return null;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
// This method is not currently used. (unsafe path not used in range formatting)
assert false;
return false;
}
@Override
public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
return doApply(exponent, output, rightIndex);
@ -279,8 +303,10 @@ public class ScientificNotation extends Notation implements Cloneable {
@Override
public int getCodePointCount() {
// This method is not used for strong modifiers.
throw new AssertionError();
// NOTE: This method is only called one place, NumberRangeFormatterImpl.
// The call site only cares about != 0 and != 1.
// Return a very large value so that if this method is used elsewhere, we should notice.
return 999;
}
@Override
@ -288,5 +314,27 @@ public class ScientificNotation extends Notation implements Cloneable {
// Scientific is always strong
return true;
}
@Override
public boolean containsField(Field field) {
// This method is not used for inner modifiers.
assert false;
return false;
}
@Override
public Parameters getParameters() {
return null;
}
@Override
public boolean semanticallyEquivalent(Modifier other) {
if (!(other instanceof ScientificModifier)) {
return false;
}
ScientificModifier _other = (ScientificModifier) other;
// TODO: Check for locale symbols and settings as well? Could be less efficient.
return exponent == _other.exponent;
}
}
}

View File

@ -0,0 +1,67 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.number;
import java.util.Locale;
import com.ibm.icu.util.ULocale;
/**
* A NumberRangeFormatter that does not yet have a locale. In order to format, a locale must be specified.
*
* @author sffc
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
* @see NumberRangeFormatter
*/
public class UnlocalizedNumberRangeFormatter extends NumberRangeFormatterSettings<UnlocalizedNumberRangeFormatter> {
/** Base constructor; called during startup only. */
UnlocalizedNumberRangeFormatter() {
super(null, KEY_MACROS, null);
}
UnlocalizedNumberRangeFormatter(NumberRangeFormatterSettings<?> parent, int key, Object value) {
super(parent, key, value);
}
/**
* Associate the given locale with the number range formatter. The locale is used for picking the
* appropriate symbols, formats, and other data for number display.
*
* <p>
* To use the Java default locale, call Locale.getDefault():
*
* <pre>
* NumberFormatter.with(). ... .locale(Locale.getDefault())
* </pre>
*
* @param locale
* The locale to use when loading data for number range formatting.
* @return The fluent chain
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
*/
public LocalizedNumberRangeFormatter locale(Locale locale) {
return new LocalizedNumberRangeFormatter(this, KEY_LOCALE, ULocale.forLocale(locale));
}
/**
* ULocale version of the {@link #locale(Locale)} setter above.
*
* @param locale
* The locale to use when loading data for number range formatting.
* @return The fluent chain
* @see #locale(Locale)
* @draft ICU 63
* @provisional This API might change or be removed in a future release.
*/
public LocalizedNumberRangeFormatter locale(ULocale locale) {
return new LocalizedNumberRangeFormatter(this, KEY_LOCALE, locale);
}
@Override
UnlocalizedNumberRangeFormatter create(int key, Object value) {
return new UnlocalizedNumberRangeFormatter(this, key, value);
}
}

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:92dc0a5ca71ac54537a6c7c42c2f80ccbd3298d9ebf69c3d732199230d5100c6
size 12487439
oid sha256:6dadae4ae83d956325f4e089327e824bb32dbf75c98e9c2815ffa7521f3505ca
size 12512661

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2308b3498ce1c2b869b60b5a0f7cea6f08aff2fac046ae260e10688c15c60b2
oid sha256:b5ffb95eb91501a9f61ee31333e392ff183e0f7dddefb592f04b90fe2ac3490c
size 92857

View File

@ -0,0 +1,726 @@
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;
import static org.junit.Assert.assertEquals;
import java.util.Locale;
import org.junit.Test;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.LocalizedNumberRangeFormatter;
import com.ibm.icu.number.Notation;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.number.NumberRangeFormatter;
import com.ibm.icu.number.NumberRangeFormatter.RangeCollapse;
import com.ibm.icu.number.NumberRangeFormatter.RangeIdentityFallback;
import com.ibm.icu.number.Precision;
import com.ibm.icu.number.UnlocalizedNumberFormatter;
import com.ibm.icu.number.UnlocalizedNumberRangeFormatter;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
/**
* @author sffc
*
*/
public class NumberRangeFormatterTest {
private static final Currency USD = Currency.getInstance("USD");
private static final Currency GBP = Currency.getInstance("GBP");
private static final Currency PTE = Currency.getInstance("PTE");
@Test
public void testSanity() {
LocalizedNumberRangeFormatter lnrf1 = NumberRangeFormatter.withLocale(ULocale.US);
LocalizedNumberRangeFormatter lnrf2 = NumberRangeFormatter.with().locale(ULocale.US);
LocalizedNumberRangeFormatter lnrf3 = NumberRangeFormatter.withLocale(Locale.US);
LocalizedNumberRangeFormatter lnrf4 = NumberRangeFormatter.with().locale(Locale.US);
assertEquals("Formatters should be equal 1", lnrf1, lnrf2);
assertEquals("Formatters should be equal 2", lnrf2, lnrf3);
assertEquals("Formatters should be equal 3", lnrf3, lnrf4);
assertEquals("Formatters should have same behavior 1", lnrf1.formatRange(4, 6), lnrf2.formatRange(4, 6));
assertEquals("Formatters should have same behavior 2", lnrf2.formatRange(4, 6), lnrf3.formatRange(4, 6));
assertEquals("Formatters should have same behavior 3", lnrf3.formatRange(4, 6), lnrf4.formatRange(4, 6));
}
@Test
public void testBasic() {
assertFormatRange(
"Basic",
NumberRangeFormatter.with(),
new ULocale("en-us"),
"15",
"~5",
"~5",
"03",
"~0",
"33,000",
"3,0005,000",
"4,9995,001",
"~5,000",
"5,0005,000,000");
assertFormatRange(
"Basic with units",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33,000 m",
"3,0005,000 m",
"4,9995,001 m",
"~5,000 m",
"5,0005,000,000 m");
assertFormatRange(
"Basic with different units",
NumberRangeFormatter.with()
.numberFormatterFirst(NumberFormatter.with().unit(MeasureUnit.METER))
.numberFormatterSecond(NumberFormatter.with().unit(MeasureUnit.KILOMETER)),
new ULocale("en-us"),
"1 m 5 km",
"5 m 5 km",
"5 m 5 km",
"0 m 3 km",
"0 m 0 km",
"3 m 3,000 km",
"3,000 m 5,000 km",
"4,999 m 5,001 km",
"5,000 m 5,000 km",
"5,000 m 5,000,000 km");
assertFormatRange(
"Basic long unit",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME)),
new ULocale("en-us"),
"15 meters",
"~5 meters",
"~5 meters",
"03 meters",
"~0 meters",
"33,000 meters",
"3,0005,000 meters",
"4,9995,001 meters",
"~5,000 meters",
"5,0005,000,000 meters");
assertFormatRange(
"Non-English locale and unit",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.FAHRENHEIT).unitWidth(UnitWidth.FULL_NAME)),
new ULocale("fr-FR"),
"15 degrés Fahrenheit",
"~5 degrés Fahrenheit",
"~5 degrés Fahrenheit",
"03 degrés Fahrenheit",
"~0 degré Fahrenheit",
"33 000 degrés Fahrenheit",
"3 0005 000 degrés Fahrenheit",
"4 9995 001 degrés Fahrenheit",
"~5 000 degrés Fahrenheit",
"5 0005 000 000 degrés Fahrenheit");
assertFormatRange(
"Locale with custom range separator",
NumberRangeFormatter.with(),
new ULocale("ja"),
"15",
"~5",
"~5",
"03",
"~0",
"33,000",
"3,0005,000",
"4,9995,001",
"~5,000",
"5,0005,000,000");
assertFormatRange(
"Locale that already has spaces around range separator",
NumberRangeFormatter.with()
.collapse(RangeCollapse.NONE)
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.KELVIN)),
new ULocale("hr"),
"1 K 5 K",
"~5 K",
"~5 K",
"0 K 3 K",
"~0 K",
"3 K 3.000 K",
"3.000 K 5.000 K",
"4.999 K 5.001 K",
"~5.000 K",
"5.000 K 5.000.000 K");
assertFormatRange(
"Locale with custom numbering system and no plural ranges data",
NumberRangeFormatter.with(),
new ULocale("shn@numbers=beng"),
// 012459 = ০১৩৪৫৯
"১–৫",
"~৫",
"~৫",
"০–৩",
"~",
"৩–৩,",
"৩,০০০–৫,",
",৯৯৯–৫,০০১",
"~৫,",
"৫,০০০–৫,,");
assertFormatRange(
"Portuguese currency",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(PTE)),
new ULocale("pt-PT"),
"1$00 - 5$00 \u200B",
"~5$00 \u200B",
"~5$00 \u200B",
"0$00 - 3$00 \u200B",
"~0$00 \u200B",
"3$00 - 3000$00 \u200B",
"3000$00 - 5000$00 \u200B",
"4999$00 - 5001$00 \u200B",
"~5000$00 \u200B",
"5000$00 - 5,000,000$00 \u200B");
}
@Test
public void testCollapse() {
assertFormatRange(
"Default collapse on currency (default rounding)",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(USD)),
new ULocale("en-us"),
"$1.00 $5.00",
"~$5.00",
"~$5.00",
"$0.00 $3.00",
"~$0.00",
"$3.00 $3,000.00",
"$3,000.00 $5,000.00",
"$4,999.00 $5,001.00",
"~$5,000.00",
"$5,000.00 $5,000,000.00");
assertFormatRange(
"Default collapse on currency",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
new ULocale("en-us"),
"$1 $5",
"~$5",
"~$5",
"$0 $3",
"~$0",
"$3 $3,000",
"$3,000 $5,000",
"$4,999 $5,001",
"~$5,000",
"$5,000 $5,000,000");
assertFormatRange(
"No collapse on currency",
NumberRangeFormatter.with()
.collapse(RangeCollapse.NONE)
.numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
new ULocale("en-us"),
"$1 $5",
"~$5",
"~$5",
"$0 $3",
"~$0",
"$3 $3,000",
"$3,000 $5,000",
"$4,999 $5,001",
"~$5,000",
"$5,000 $5,000,000");
assertFormatRange(
"Unit collapse on currency",
NumberRangeFormatter.with()
.collapse(RangeCollapse.UNIT)
.numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
new ULocale("en-us"),
"$15",
"~$5",
"~$5",
"$03",
"~$0",
"$33,000",
"$3,0005,000",
"$4,9995,001",
"~$5,000",
"$5,0005,000,000");
assertFormatRange(
"All collapse on currency",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with().unit(USD).precision(Precision.integer())),
new ULocale("en-us"),
"$15",
"~$5",
"~$5",
"$03",
"~$0",
"$33,000",
"$3,0005,000",
"$4,9995,001",
"~$5,000",
"$5,0005,000,000");
assertFormatRange(
"Default collapse on currency ISO code",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with()
.unit(GBP)
.unitWidth(UnitWidth.ISO_CODE)
.precision(Precision.integer())),
new ULocale("en-us"),
"GBP 15",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 03",
"~GBP 0",
"GBP 33,000",
"GBP 3,0005,000",
"GBP 4,9995,001",
"~GBP 5,000",
"GBP 5,0005,000,000");
assertFormatRange(
"No collapse on currency ISO code",
NumberRangeFormatter.with()
.collapse(RangeCollapse.NONE)
.numberFormatterBoth(NumberFormatter.with()
.unit(GBP)
.unitWidth(UnitWidth.ISO_CODE)
.precision(Precision.integer())),
new ULocale("en-us"),
"GBP 1 GBP 5",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 0 GBP 3",
"~GBP 0",
"GBP 3 GBP 3,000",
"GBP 3,000 GBP 5,000",
"GBP 4,999 GBP 5,001",
"~GBP 5,000",
"GBP 5,000 GBP 5,000,000");
assertFormatRange(
"Unit collapse on currency ISO code",
NumberRangeFormatter.with()
.collapse(RangeCollapse.UNIT)
.numberFormatterBoth(NumberFormatter.with()
.unit(GBP)
.unitWidth(UnitWidth.ISO_CODE)
.precision(Precision.integer())),
new ULocale("en-us"),
"GBP 15",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 03",
"~GBP 0",
"GBP 33,000",
"GBP 3,0005,000",
"GBP 4,9995,001",
"~GBP 5,000",
"GBP 5,0005,000,000");
assertFormatRange(
"All collapse on currency ISO code",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with()
.unit(GBP)
.unitWidth(UnitWidth.ISO_CODE)
.precision(Precision.integer())),
new ULocale("en-us"),
"GBP 15",
"~GBP 5", // TODO: Fix this at some point
"~GBP 5",
"GBP 03",
"~GBP 0",
"GBP 33,000",
"GBP 3,0005,000",
"GBP 4,9995,001",
"~GBP 5,000",
"GBP 5,0005,000,000");
// Default collapse on measurement unit is in testBasic()
assertFormatRange(
"No collapse on measurement unit",
NumberRangeFormatter.with()
.collapse(RangeCollapse.NONE)
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
new ULocale("en-us"),
"1 m 5 m",
"~5 m",
"~5 m",
"0 m 3 m",
"~0 m",
"3 m 3,000 m",
"3,000 m 5,000 m",
"4,999 m 5,001 m",
"~5,000 m",
"5,000 m 5,000,000 m");
assertFormatRange(
"Unit collapse on measurement unit",
NumberRangeFormatter.with()
.collapse(RangeCollapse.UNIT)
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33,000 m",
"3,0005,000 m",
"4,9995,001 m",
"~5,000 m",
"5,0005,000,000 m");
assertFormatRange(
"All collapse on measurement unit",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with().unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33,000 m",
"3,0005,000 m",
"4,9995,001 m",
"~5,000 m",
"5,0005,000,000 m");
assertFormatRange(
"Default collapse, long-form compact notation",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactLong())),
new ULocale("de-CH"),
"15",
"~5",
"~5",
"03",
"~0",
"33 Tausend",
"35 Tausend",
"~5 Tausend",
"~5 Tausend",
"5 Tausend 5 Millionen");
assertFormatRange(
"Unit collapse, long-form compact notation",
NumberRangeFormatter.with()
.collapse(RangeCollapse.UNIT)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactLong())),
new ULocale("de-CH"),
"15",
"~5",
"~5",
"03",
"~0",
"33 Tausend",
"3 Tausend 5 Tausend",
"~5 Tausend",
"~5 Tausend",
"5 Tausend 5 Millionen");
assertFormatRange(
"Default collapse on measurement unit with compact-short notation",
NumberRangeFormatter.with()
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33K m",
"3K 5K m",
"~5K m",
"~5K m",
"5K 5M m");
assertFormatRange(
"No collapse on measurement unit with compact-short notation",
NumberRangeFormatter.with()
.collapse(RangeCollapse.NONE)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
new ULocale("en-us"),
"1 m 5 m",
"~5 m",
"~5 m",
"0 m 3 m",
"~0 m",
"3 m 3K m",
"3K m 5K m",
"~5K m",
"~5K m",
"5K m 5M m");
assertFormatRange(
"Unit collapse on measurement unit with compact-short notation",
NumberRangeFormatter.with()
.collapse(RangeCollapse.UNIT)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33K m",
"3K 5K m",
"~5K m",
"~5K m",
"5K 5M m");
assertFormatRange(
"All collapse on measurement unit with compact-short notation",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort()).unit(MeasureUnit.METER)),
new ULocale("en-us"),
"15 m",
"~5 m",
"~5 m",
"03 m",
"~0 m",
"33K m",
"35K m", // this one is the key use case for ALL
"~5K m",
"~5K m",
"5K 5M m");
assertFormatRange(
"No collapse on scientific notation",
NumberRangeFormatter.with()
.collapse(RangeCollapse.NONE)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.scientific())),
new ULocale("en-us"),
"1E0 5E0",
"~5E0",
"~5E0",
"0E0 3E0",
"~0E0",
"3E0 3E3",
"3E3 5E3",
"4.999E3 5.001E3",
"~5E3",
"5E3 5E6");
assertFormatRange(
"All collapse on scientific notation",
NumberRangeFormatter.with()
.collapse(RangeCollapse.ALL)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.scientific())),
new ULocale("en-us"),
"15E0",
"~5E0",
"~5E0",
"03E0",
"~0E0",
"3E0 3E3",
"35E3",
"4.9995.001E3",
"~5E3",
"5E3 5E6");
// TODO: Test compact currency?
// The code is not smart enough to differentiate the notation from the unit.
}
@Test
public void testIdentity() {
assertFormatRange(
"Identity fallback Range",
NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.RANGE),
new ULocale("en-us"),
"15",
"55",
"55",
"03",
"00",
"33,000",
"3,0005,000",
"4,9995,001",
"5,0005,000",
"5,0005,000,000");
assertFormatRange(
"Identity fallback Approximately or Single Value",
NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE),
new ULocale("en-us"),
"15",
"~5",
"5",
"03",
"0",
"33,000",
"3,0005,000",
"4,9995,001",
"5,000",
"5,0005,000,000");
assertFormatRange(
"Identity fallback Single Value",
NumberRangeFormatter.with().identityFallback(RangeIdentityFallback.SINGLE_VALUE),
new ULocale("en-us"),
"15",
"5",
"5",
"03",
"0",
"33,000",
"3,0005,000",
"4,9995,001",
"5,000",
"5,0005,000,000");
assertFormatRange(
"Identity fallback Approximately or Single Value with compact notation",
NumberRangeFormatter.with()
.identityFallback(RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)
.numberFormatterBoth(NumberFormatter.with().notation(Notation.compactShort())),
new ULocale("en-us"),
"15",
"~5",
"5",
"03",
"0",
"33K",
"3K 5K",
"~5K",
"5K",
"5K 5M");
}
@Test
public void testDifferentFormatters() {
assertFormatRange(
"Different rounding rules",
NumberRangeFormatter.with()
.numberFormatterFirst(NumberFormatter.with().precision(Precision.integer()))
.numberFormatterSecond(NumberFormatter.with().precision(Precision.fixedDigits(2))),
new ULocale("en-us"),
"15.0",
"55.0",
"55.0",
"03.0",
"00.0",
"33,000",
"3,0005,000",
"4,9995,000",
"5,0005,000", // TODO: Should this one be ~5,000?
"5,0005,000,000");
}
@Test
public void testPlurals() {
// Locale sl has interesting plural forms:
// GBP{
// one{"britanski funt"}
// two{"britanska funta"}
// few{"britanski funti"}
// other{"britanskih funtov"}
// }
ULocale locale = new ULocale("sl");
UnlocalizedNumberFormatter unf = NumberFormatter.with()
.unit(GBP)
.unitWidth(UnitWidth.FULL_NAME)
.precision(Precision.integer());
LocalizedNumberFormatter lnf = unf.locale(locale);
// For comparison, run the non-range version of the formatter
assertEquals(Integer.toString(1), "1 britanski funt", lnf.format(1).toString());
assertEquals(Integer.toString(2), "2 britanska funta", lnf.format(2).toString());
assertEquals(Integer.toString(3), "3 britanski funti", lnf.format(3).toString());
assertEquals(Integer.toString(5), "5 britanskih funtov", lnf.format(5).toString());
LocalizedNumberRangeFormatter lnrf = NumberRangeFormatter.with()
.numberFormatterBoth(unf)
.identityFallback(RangeIdentityFallback.RANGE)
.locale(locale);
Object[][] cases = new Object[][] {
{1, 1, "11 britanski funti"}, // one + one -> few
{1, 2, "12 britanska funta"}, // one + two -> two
{1, 3, "13 britanski funti"}, // one + few -> few
{1, 5, "15 britanskih funtov"}, // one + other -> other
{2, 1, "21 britanski funti"}, // two + one -> few
{2, 2, "22 britanska funta"}, // two + two -> two
{2, 3, "23 britanski funti"}, // two + few -> few
{2, 5, "25 britanskih funtov"}, // two + other -> other
{3, 1, "31 britanski funti"}, // few + one -> few
{3, 2, "32 britanska funta"}, // few + two -> two
{3, 3, "33 britanski funti"}, // few + few -> few
{3, 5, "35 britanskih funtov"}, // few + other -> other
{5, 1, "51 britanski funti"}, // other + one -> few
{5, 2, "52 britanska funta"}, // other + two -> two
{5, 3, "53 britanski funti"}, // other + few -> few
{5, 5, "55 britanskih funtov"}, // other + other -> other
};
for (Object[] cas : cases) {
int first = (Integer) cas[0];
int second = (Integer) cas[1];
String expected = (String) cas[2];
String message = Integer.toString(first) + " " + Integer.toString(second);
String actual = lnrf.formatRange(first, second).toString();
assertEquals(message, expected, actual);
}
}
static void assertFormatRange(
String message,
UnlocalizedNumberRangeFormatter f,
ULocale locale,
String expected_10_50,
String expected_49_51,
String expected_50_50,
String expected_00_30,
String expected_00_00,
String expected_30_3K,
String expected_30K_50K,
String expected_49K_51K,
String expected_50K_50K,
String expected_50K_50M) {
LocalizedNumberRangeFormatter l = f.locale(locale);
assertFormattedRangeEquals(message, l, 1, 5, expected_10_50);
assertFormattedRangeEquals(message, l, 4.9999999, 5.0000001, expected_49_51);
assertFormattedRangeEquals(message, l, 5, 5, expected_50_50);
assertFormattedRangeEquals(message, l, 0, 3, expected_00_30);
assertFormattedRangeEquals(message, l, 0, 0, expected_00_00);
assertFormattedRangeEquals(message, l, 3, 3000, expected_30_3K);
assertFormattedRangeEquals(message, l, 3000, 5000, expected_30K_50K);
assertFormattedRangeEquals(message, l, 4999, 5001, expected_49K_51K);
assertFormattedRangeEquals(message, l, 5000, 5000, expected_50K_50K);
assertFormattedRangeEquals(message, l, 5e3, 5e6, expected_50K_50M);
}
private static void assertFormattedRangeEquals(String message, LocalizedNumberRangeFormatter l, Number first,
Number second, String expected) {
String actual = l.formatRange(first, second).toString();
assertEquals(message + ": " + first + ", " + second, expected, actual);
}
}