1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // © 2018 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 package ohos.global.icu.number; 5 6 import java.util.MissingResourceException; 7 8 import ohos.global.icu.impl.FormattedStringBuilder; 9 import ohos.global.icu.impl.ICUData; 10 import ohos.global.icu.impl.ICUResourceBundle; 11 import ohos.global.icu.impl.PatternProps; 12 import ohos.global.icu.impl.SimpleFormatterImpl; 13 import ohos.global.icu.impl.StandardPlural; 14 import ohos.global.icu.impl.UResource; 15 import ohos.global.icu.impl.number.DecimalQuantity; 16 import ohos.global.icu.impl.number.MicroProps; 17 import ohos.global.icu.impl.number.Modifier; 18 import ohos.global.icu.impl.number.SimpleModifier; 19 import ohos.global.icu.impl.number.range.PrefixInfixSuffixLengthHelper; 20 import ohos.global.icu.impl.number.range.RangeMacroProps; 21 import ohos.global.icu.impl.number.range.StandardPluralRanges; 22 import ohos.global.icu.number.NumberRangeFormatter.RangeCollapse; 23 import ohos.global.icu.number.NumberRangeFormatter.RangeIdentityFallback; 24 import ohos.global.icu.number.NumberRangeFormatter.RangeIdentityResult; 25 import ohos.global.icu.text.NumberFormat; 26 import ohos.global.icu.util.ULocale; 27 import ohos.global.icu.util.UResourceBundle; 28 29 /** 30 * Business logic behind NumberRangeFormatter. 31 */ 32 class NumberRangeFormatterImpl { 33 34 final NumberFormatterImpl formatterImpl1; 35 final NumberFormatterImpl formatterImpl2; 36 final boolean fSameFormatters; 37 38 final NumberRangeFormatter.RangeCollapse fCollapse; 39 final NumberRangeFormatter.RangeIdentityFallback fIdentityFallback; 40 41 // Should be final, but they are set in a helper function, not the constructor proper. 42 // TODO: Clean up to make these fields actually final. 43 /* final */ String fRangePattern; 44 /* final */ SimpleModifier fApproximatelyModifier; 45 46 final StandardPluralRanges fPluralRanges; 47 48 //////////////////// 49 50 // Helper function for 2-dimensional switch statement identity2d(RangeIdentityFallback a, RangeIdentityResult b)51 int identity2d(RangeIdentityFallback a, RangeIdentityResult b) { 52 return a.ordinal() | (b.ordinal() << 4); 53 } 54 55 private static final class NumberRangeDataSink extends UResource.Sink { 56 57 String rangePattern; 58 String approximatelyPattern; 59 60 // For use with SimpleFormatterImpl 61 StringBuilder sb; 62 NumberRangeDataSink(StringBuilder sb)63 NumberRangeDataSink(StringBuilder sb) { 64 this.sb = sb; 65 } 66 67 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)68 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 69 UResource.Table miscTable = value.getTable(); 70 for (int i = 0; miscTable.getKeyAndValue(i, key, value); ++i) { 71 if (key.contentEquals("range") && !hasRangeData()) { 72 String pattern = value.getString(); 73 rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2); 74 } 75 if (key.contentEquals("approximately") && !hasApproxData()) { 76 String pattern = value.getString(); 77 approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 1, 1); // 1 arg, as in "~{0}" 78 } 79 } 80 } 81 hasRangeData()82 private boolean hasRangeData() { 83 return rangePattern != null; 84 } 85 hasApproxData()86 private boolean hasApproxData() { 87 return approximatelyPattern != null; 88 } 89 isComplete()90 public boolean isComplete() { 91 return hasRangeData() && hasApproxData(); 92 } 93 fillInDefaults()94 public void fillInDefaults() { 95 if (!hasRangeData()) { 96 rangePattern = SimpleFormatterImpl.compileToStringMinMaxArguments("{0}–{1}", sb, 2, 2); 97 } 98 if (!hasApproxData()) { 99 approximatelyPattern = SimpleFormatterImpl.compileToStringMinMaxArguments("~{0}", sb, 1, 1); 100 } 101 } 102 } 103 getNumberRangeData( ULocale locale, String nsName, NumberRangeFormatterImpl out)104 private static void getNumberRangeData( 105 ULocale locale, 106 String nsName, 107 NumberRangeFormatterImpl out) { 108 StringBuilder sb = new StringBuilder(); 109 NumberRangeDataSink sink = new NumberRangeDataSink(sb); 110 ICUResourceBundle resource; 111 resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); 112 sb.append("NumberElements/"); 113 sb.append(nsName); 114 sb.append("/miscPatterns"); 115 String key = sb.toString(); 116 try { 117 resource.getAllItemsWithFallback(key, sink); 118 } catch (MissingResourceException e) { 119 // ignore; fall back to latn 120 } 121 122 // Fall back to latn if necessary 123 if (!sink.isComplete()) { 124 resource.getAllItemsWithFallback("NumberElements/latn/miscPatterns", sink); 125 } 126 127 sink.fillInDefaults(); 128 129 out.fRangePattern = sink.rangePattern; 130 out.fApproximatelyModifier = new SimpleModifier(sink.approximatelyPattern, null, false); 131 } 132 133 //////////////////// 134 NumberRangeFormatterImpl(RangeMacroProps macros)135 public NumberRangeFormatterImpl(RangeMacroProps macros) { 136 formatterImpl1 = new NumberFormatterImpl(macros.formatter1 != null ? macros.formatter1.resolve() 137 : NumberFormatter.withLocale(macros.loc).resolve()); 138 formatterImpl2 = new NumberFormatterImpl(macros.formatter2 != null ? macros.formatter2.resolve() 139 : NumberFormatter.withLocale(macros.loc).resolve()); 140 fSameFormatters = macros.sameFormatters != 0; 141 fCollapse = macros.collapse != null ? macros.collapse : NumberRangeFormatter.RangeCollapse.AUTO; 142 fIdentityFallback = macros.identityFallback != null ? macros.identityFallback 143 : NumberRangeFormatter.RangeIdentityFallback.APPROXIMATELY; 144 145 String nsName = formatterImpl1.getRawMicroProps().nsName; 146 if (nsName == null || !nsName.equals(formatterImpl2.getRawMicroProps().nsName)) { 147 throw new IllegalArgumentException("Both formatters must have same numbering system"); 148 } 149 getNumberRangeData(macros.loc, nsName, this); 150 151 // TODO: Get locale from PluralRules instead? 152 fPluralRanges = new StandardPluralRanges(macros.loc); 153 } 154 format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding)155 public FormattedNumberRange format(DecimalQuantity quantity1, DecimalQuantity quantity2, boolean equalBeforeRounding) { 156 FormattedStringBuilder string = new FormattedStringBuilder(); 157 MicroProps micros1 = formatterImpl1.preProcess(quantity1); 158 MicroProps micros2; 159 if (fSameFormatters) { 160 micros2 = formatterImpl1.preProcess(quantity2); 161 } else { 162 micros2 = formatterImpl2.preProcess(quantity2); 163 } 164 165 // If any of the affixes are different, an identity is not possible 166 // and we must use formatRange(). 167 // TODO: Write this as MicroProps operator==() ? 168 // TODO: Avoid the redundancy of these equality operations with the 169 // ones in formatRange? 170 if (!micros1.modInner.semanticallyEquivalent(micros2.modInner) 171 || !micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle) 172 || !micros1.modOuter.semanticallyEquivalent(micros2.modOuter)) { 173 formatRange(quantity1, quantity2, string, micros1, micros2); 174 return new FormattedNumberRange(string, quantity1, quantity2, RangeIdentityResult.NOT_EQUAL); 175 } 176 177 // Check for identity 178 RangeIdentityResult identityResult; 179 if (equalBeforeRounding) { 180 identityResult = RangeIdentityResult.EQUAL_BEFORE_ROUNDING; 181 } else if (quantity1.equals(quantity2)) { 182 identityResult = RangeIdentityResult.EQUAL_AFTER_ROUNDING; 183 } else { 184 identityResult = RangeIdentityResult.NOT_EQUAL; 185 } 186 187 // Java does not let us use a constexpr like C++; 188 // we need to expand identity2d calls. 189 switch (identity2d(fIdentityFallback, identityResult)) { 190 case (3 | (2 << 4)): // RANGE, NOT_EQUAL 191 case (3 | (1 << 4)): // RANGE, EQUAL_AFTER_ROUNDING 192 case (3 | (0 << 4)): // RANGE, EQUAL_BEFORE_ROUNDING 193 case (2 | (2 << 4)): // APPROXIMATELY, NOT_EQUAL 194 case (1 | (2 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, NOT_EQUAL 195 case (0 | (2 << 4)): // SINGLE_VALUE, NOT_EQUAL 196 formatRange(quantity1, quantity2, string, micros1, micros2); 197 break; 198 199 case (2 | (1 << 4)): // APPROXIMATELY, EQUAL_AFTER_ROUNDING 200 case (2 | (0 << 4)): // APPROXIMATELY, EQUAL_BEFORE_ROUNDING 201 case (1 | (1 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_AFTER_ROUNDING 202 formatApproximately(quantity1, quantity2, string, micros1, micros2); 203 break; 204 205 case (1 | (0 << 4)): // APPROXIMATE_OR_SINGLE_VALUE, EQUAL_BEFORE_ROUNDING 206 case (0 | (1 << 4)): // SINGLE_VALUE, EQUAL_AFTER_ROUNDING 207 case (0 | (0 << 4)): // SINGLE_VALUE, EQUAL_BEFORE_ROUNDING 208 formatSingleValue(quantity1, quantity2, string, micros1, micros2); 209 break; 210 211 default: 212 assert false; 213 break; 214 } 215 216 return new FormattedNumberRange(string, quantity1, quantity2, identityResult); 217 } 218 formatSingleValue(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string, MicroProps micros1, MicroProps micros2)219 private void formatSingleValue(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string, 220 MicroProps micros1, MicroProps micros2) { 221 if (fSameFormatters) { 222 int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0); 223 NumberFormatterImpl.writeAffixes(micros1, string, 0, length); 224 } else { 225 formatRange(quantity1, quantity2, string, micros1, micros2); 226 } 227 228 } 229 formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string, MicroProps micros1, MicroProps micros2)230 private void formatApproximately(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string, 231 MicroProps micros1, MicroProps micros2) { 232 if (fSameFormatters) { 233 int length = NumberFormatterImpl.writeNumber(micros1, quantity1, string, 0); 234 // HEURISTIC: Desired modifier order: inner, middle, approximately, outer. 235 length += micros1.modInner.apply(string, 0, length); 236 length += micros1.modMiddle.apply(string, 0, length); 237 length += fApproximatelyModifier.apply(string, 0, length); 238 micros1.modOuter.apply(string, 0, length); 239 } else { 240 formatRange(quantity1, quantity2, string, micros1, micros2); 241 } 242 } 243 formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string, MicroProps micros1, MicroProps micros2)244 private void formatRange(DecimalQuantity quantity1, DecimalQuantity quantity2, FormattedStringBuilder string, 245 MicroProps micros1, MicroProps micros2) { 246 // modInner is always notation (scientific); collapsable in ALL. 247 // modOuter is always units; collapsable in ALL, AUTO, and UNIT. 248 // modMiddle could be either; collapsable in ALL and sometimes AUTO and UNIT. 249 // Never collapse an outer mod but not an inner mod. 250 boolean collapseOuter, collapseMiddle, collapseInner; 251 switch (fCollapse) { 252 case ALL: 253 case AUTO: 254 case UNIT: 255 { 256 // OUTER MODIFIER 257 collapseOuter = micros1.modOuter.semanticallyEquivalent(micros2.modOuter); 258 259 if (!collapseOuter) { 260 // Never collapse inner mods if outer mods are not collapsable 261 collapseMiddle = false; 262 collapseInner = false; 263 break; 264 } 265 266 // MIDDLE MODIFIER 267 collapseMiddle = micros1.modMiddle.semanticallyEquivalent(micros2.modMiddle); 268 269 if (!collapseMiddle) { 270 // Never collapse inner mods if outer mods are not collapsable 271 collapseInner = false; 272 break; 273 } 274 275 // MIDDLE MODIFIER HEURISTICS 276 // (could disable collapsing of the middle modifier) 277 // The modifiers are equal by this point, so we can look at just one of them. 278 Modifier mm = micros1.modMiddle; 279 if (fCollapse == RangeCollapse.UNIT) { 280 // Only collapse if the modifier is a unit. 281 // TODO: Make a better way to check for a unit? 282 // TODO: Handle case where the modifier has both notation and unit (compact currency)? 283 if (!mm.containsField(NumberFormat.Field.CURRENCY) && !mm.containsField(NumberFormat.Field.PERCENT)) { 284 collapseMiddle = false; 285 } 286 } else if (fCollapse == RangeCollapse.AUTO) { 287 // Heuristic as of ICU 63: collapse only if the modifier is more than one code point. 288 if (mm.getCodePointCount() <= 1) { 289 collapseMiddle = false; 290 } 291 } 292 293 if (!collapseMiddle || fCollapse != RangeCollapse.ALL) { 294 collapseInner = false; 295 break; 296 } 297 298 // INNER MODIFIER 299 collapseInner = micros1.modInner.semanticallyEquivalent(micros2.modInner); 300 301 // All done checking for collapsability. 302 break; 303 } 304 305 default: 306 collapseOuter = false; 307 collapseMiddle = false; 308 collapseInner = false; 309 break; 310 } 311 312 // Java doesn't have macros, constexprs, or stack objects. 313 // Use a helper object instead. 314 PrefixInfixSuffixLengthHelper h = new PrefixInfixSuffixLengthHelper(); 315 316 SimpleModifier.formatTwoArgPattern(fRangePattern, string, 0, h, null); 317 assert h.lengthInfix > 0; 318 319 // SPACING HEURISTIC 320 // Add spacing unless all modifiers are collapsed. 321 // TODO: add API to control this? 322 // TODO: Use a data-driven heuristic like currency spacing? 323 // TODO: Use Unicode [:whitespace:] instead of PatternProps whitespace? (consider speed implications) 324 { 325 boolean repeatInner = !collapseInner && micros1.modInner.getCodePointCount() > 0; 326 boolean repeatMiddle = !collapseMiddle && micros1.modMiddle.getCodePointCount() > 0; 327 boolean repeatOuter = !collapseOuter && micros1.modOuter.getCodePointCount() > 0; 328 if (repeatInner || repeatMiddle || repeatOuter) { 329 // Add spacing if there is not already spacing 330 if (!PatternProps.isWhiteSpace(string.charAt(h.index1()))) { 331 h.lengthInfix += string.insertCodePoint(h.index1(), '\u0020', null); 332 } 333 if (!PatternProps.isWhiteSpace(string.charAt(h.index2() - 1))) { 334 h.lengthInfix += string.insertCodePoint(h.index2(), '\u0020', null); 335 } 336 } 337 } 338 339 h.length1 += NumberFormatterImpl.writeNumber(micros1, quantity1, string, h.index0()); 340 h.length2 += NumberFormatterImpl.writeNumber(micros2, quantity2, string, h.index2()); 341 342 // TODO: Support padding? 343 344 if (collapseInner) { 345 // Note: this is actually a mix of prefix and suffix, but adding to infix length works 346 Modifier mod = resolveModifierPlurals(micros1.modInner, micros2.modInner); 347 h.lengthInfix += mod.apply(string, h.index0(), h.index3()); 348 } else { 349 h.length1 += micros1.modInner.apply(string, h.index0(), h.index1()); 350 h.length2 += micros2.modInner.apply(string, h.index2(), h.index3()); 351 } 352 353 if (collapseMiddle) { 354 // Note: this is actually a mix of prefix and suffix, but adding to infix length works 355 Modifier mod = resolveModifierPlurals(micros1.modMiddle, micros2.modMiddle); 356 h.lengthInfix += mod.apply(string, h.index0(), h.index3()); 357 } else { 358 h.length1 += micros1.modMiddle.apply(string, h.index0(), h.index1()); 359 h.length2 += micros2.modMiddle.apply(string, h.index2(), h.index3()); 360 } 361 362 if (collapseOuter) { 363 // Note: this is actually a mix of prefix and suffix, but adding to infix length works 364 Modifier mod = resolveModifierPlurals(micros1.modOuter, micros2.modOuter); 365 h.lengthInfix += mod.apply(string, h.index0(), h.index3()); 366 } else { 367 h.length1 += micros1.modOuter.apply(string, h.index0(), h.index1()); 368 h.length2 += micros2.modOuter.apply(string, h.index2(), h.index3()); 369 } 370 } 371 resolveModifierPlurals(Modifier first, Modifier second)372 Modifier resolveModifierPlurals(Modifier first, Modifier second) { 373 Modifier.Parameters firstParameters = first.getParameters(); 374 if (firstParameters == null) { 375 // No plural form; return a fallback (e.g., the first) 376 return first; 377 } 378 379 Modifier.Parameters secondParameters = second.getParameters(); 380 if (secondParameters == null) { 381 // No plural form; return a fallback (e.g., the first) 382 return first; 383 } 384 385 // Get the required plural form from data 386 StandardPlural resultPlural = fPluralRanges.resolve(firstParameters.plural, secondParameters.plural); 387 388 // Get and return the new Modifier 389 assert firstParameters.obj == secondParameters.obj; 390 assert firstParameters.signum == secondParameters.signum; 391 Modifier mod = firstParameters.obj.getModifier(firstParameters.signum, resultPlural); 392 assert mod != null; 393 return mod; 394 } 395 396 } 397