/* GENERATED SOURCE. DO NOT MODIFY. */ // © 2018 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html package android.icu.number; import java.util.MissingResourceException; import android.icu.impl.FormattedStringBuilder; import android.icu.impl.FormattedValueStringBuilderImpl; import android.icu.impl.ICUData; import android.icu.impl.ICUResourceBundle; import android.icu.impl.PatternProps; import android.icu.impl.SimpleFormatterImpl; import android.icu.impl.StandardPlural; import android.icu.impl.UResource; import android.icu.impl.number.DecimalQuantity; import android.icu.impl.number.MacroProps; import android.icu.impl.number.MicroProps; import android.icu.impl.number.Modifier; import android.icu.impl.number.SimpleModifier; import android.icu.impl.number.range.PrefixInfixSuffixLengthHelper; import android.icu.impl.number.range.RangeMacroProps; import android.icu.impl.number.range.StandardPluralRanges; import android.icu.number.NumberRangeFormatter.RangeCollapse; import android.icu.number.NumberRangeFormatter.RangeIdentityFallback; import android.icu.number.NumberRangeFormatter.RangeIdentityResult; import android.icu.text.NumberFormat; import android.icu.util.ULocale; import android.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 it is set in a helper function, not the constructor proper. // TODO: Clean up to make this field actually final. /* final */ String fRangePattern; final NumberFormatterImpl fApproximatelyFormatter; 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; // Note: approximatelyPattern is unused since ICU 69. // 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") && !hasRangeData()) { String pattern = value.getString(); rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); } /* // Note: approximatelyPattern is unused since ICU 69. if (key.contentEquals("approximately") && !hasApproxData()) { String pattern = value.getString(); approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 1, 1); // 1 arg, as in "~{0}" } */ } } private boolean hasRangeData() { return rangePattern != null; } /* // Note: approximatelyPattern is unused since ICU 69. private boolean hasApproxData() { return approximatelyPattern != null; } */ public boolean isComplete() { return hasRangeData() /* && hasApproxData() */; } public void fillInDefaults() { if (!hasRangeData()) { rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}–{1}", sb, 2, 2); } /* if (!hasApproxData()) { approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1); } */ } } 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(); try { resource.getAllItemsWithFallback(key, sink); } catch (MissingResourceException e) { // ignore; fall back to latn } // Fall back to latn if necessary if (!sink.isComplete()) { resource.getAllItemsWithFallback("NumberElements/latn/miscPatterns", sink); } sink.fillInDefaults(); out.fRangePattern = sink.rangePattern; // out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false); } //////////////////// public NumberRangeFormatterImpl(RangeMacroProps macros) { LocalizedNumberFormatter formatter1 = macros.formatter1 != null ? macros.formatter1.locale(macros.loc) : NumberFormatter.withLocale(macros.loc); LocalizedNumberFormatter formatter2 = macros.formatter2 != null ? macros.formatter2.locale(macros.loc) : NumberFormatter.withLocale(macros.loc); formatterImpl1 = new NumberFormatterImpl(formatter1.resolve()); formatterImpl2 = new NumberFormatterImpl(formatter2.resolve()); fSameFormatters = macros.sameFormatters != 0; fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO; fIdentityFallback = macros.identityFallback != null ? macros.identityFallback : NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY; String nsName = formatterImpl1.getRawMicroProps().nsName; if (nsName == null || (!fSameFormatters && !nsName.equals(formatterImpl2.getRawMicroProps().nsName))) { throw new IllegalArgumentException("Both formatters must have same numbering system"); } getNumberRangeData(macros.loc, nsName, this); if (fSameFormatters && ( fIdentityFallback == RangeIdentityFallback.APPROXIMATELY || fIdentityFallback == RangeIdentityFallback.APPROXIMATELY_OR_SINGLE_VALUE)) { MacroProps approximatelyMacros = new MacroProps(); approximatelyMacros.approximately = true; fApproximatelyFormatter = new NumberFormatterImpl( formatter1.macros(approximatelyMacros).resolve()); } else { fApproximatelyFormatter = null; } // TODO: Get locale from PluralRules instead? fPluralRanges = StandardPluralRanges.forLocale(macros.loc); } public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) { FormattedStringBuilder string = new FormattedStringBuilder(); 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, FormattedStringBuilder 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, FormattedStringBuilder string, MicroProps micros1, MicroProps micros2) { if (fSameFormatters) { // Re-format using the approximately formatter: quantity1.resetExponent(); MicroProps microsAppx = fApproximatelyFormatter.preProcess(quantity1); int length = NumberFormatterImpl.writeNumber(microsAppx, quantity1, string, 0); // HEURISTIC: Desired modifier order: inner, middle, approximately, outer. length += microsAppx.modInner.apply(string, 0, length); length += microsAppx.modMiddle.apply(string, 0, length); microsAppx.modOuter.apply(string, 0, length); } else { formatRange(quantity1, quantity2, string, micros1, micros2); } } private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder 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 collapsibility. 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()); // ICU-21684: Write the second number to a temp string to avoid repeated insert operations FormattedStringBuilder tempString = new FormattedStringBuilder(); NumberFormatterImpl.writeNumber(micros2, quantity2, tempString, 0); h.length2 += string.insert(h.index2(), tempString); // TODO: Support padding? if (collapseInner) { Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner); h.lengthSuffix += mod.apply(string, h.index0(), h.index4()); h.lengthPrefix += mod.getPrefixLength(); h.lengthSuffix -= mod.getPrefixLength(); } else { h.length1 += micros1.modInner.apply(string, h.index0(), h.index1()); h.length2 += micros2.modInner.apply(string, h.index2(), h.index4()); } if (collapseMiddle) { Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle); h.lengthSuffix += mod.apply(string, h.index0(), h.index4()); h.lengthPrefix += mod.getPrefixLength(); h.lengthSuffix -= mod.getPrefixLength(); } else { h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1()); h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index4()); } if (collapseOuter) { Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter); h.lengthSuffix += mod.apply(string, h.index0(), h.index4()); h.lengthPrefix += mod.getPrefixLength(); h.lengthSuffix -= mod.getPrefixLength(); } else { h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1()); h.length2 += micros2.modOuter.apply(string, h.index2(), h.index4()); } // Now that all pieces are added, save the span info. FormattedValueStringBuilderImpl.applySpanRange( string, NumberRangeFormatter.SpanField.NUMBER_RANGE_SPAN, 0, h.index0(), h.index1()); FormattedValueStringBuilderImpl.applySpanRange( string, NumberRangeFormatter.SpanField.NUMBER_RANGE_SPAN, 1, 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 Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural); assert mod != null; return mod; } }