1 package org.unicode.cldr.test; 2 3 import java.io.PrintWriter; 4 import java.io.StringWriter; 5 import java.text.ChoiceFormat; 6 import java.util.ArrayList; 7 import java.util.BitSet; 8 import java.util.Collection; 9 import java.util.Date; 10 import java.util.HashMap; 11 import java.util.LinkedHashSet; 12 import java.util.List; 13 import java.util.Locale; 14 import java.util.Map; 15 import java.util.Objects; 16 import java.util.Set; 17 import java.util.regex.Matcher; 18 import java.util.regex.Pattern; 19 20 import org.unicode.cldr.tool.CLDRFileTransformer; 21 import org.unicode.cldr.tool.CLDRFileTransformer.LocaleTransform; 22 import org.unicode.cldr.tool.LikelySubtags; 23 import org.unicode.cldr.util.AnnotationUtil; 24 import org.unicode.cldr.util.CLDRConfig; 25 import org.unicode.cldr.util.CLDRFile; 26 import org.unicode.cldr.util.CLDRLocale; 27 import org.unicode.cldr.util.CLDRPaths; 28 import org.unicode.cldr.util.CldrUtility; 29 import org.unicode.cldr.util.DayPeriodInfo; 30 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod; 31 import org.unicode.cldr.util.EmojiConstants; 32 import org.unicode.cldr.util.Factory; 33 import org.unicode.cldr.util.GrammarInfo; 34 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature; 35 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope; 36 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget; 37 import org.unicode.cldr.util.ICUServiceBuilder; 38 import org.unicode.cldr.util.LanguageTagParser; 39 import org.unicode.cldr.util.Level; 40 import org.unicode.cldr.util.PathDescription; 41 import org.unicode.cldr.util.PatternCache; 42 import org.unicode.cldr.util.PluralSamples; 43 import org.unicode.cldr.util.StandardCodes.LstrType; 44 import org.unicode.cldr.util.SupplementalDataInfo; 45 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo; 46 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count; 47 import org.unicode.cldr.util.SupplementalDataInfo.PluralType; 48 import org.unicode.cldr.util.TransliteratorUtilities; 49 import org.unicode.cldr.util.UnitConverter; 50 import org.unicode.cldr.util.Units; 51 import org.unicode.cldr.util.Validity; 52 import org.unicode.cldr.util.Validity.Status; 53 import org.unicode.cldr.util.XListFormatter.ListTypeLength; 54 import org.unicode.cldr.util.XPathParts; 55 import org.unicode.cldr.util.personname.PersonNameFormatter; 56 import org.unicode.cldr.util.personname.PersonNameFormatter.FallbackFormatter; 57 import org.unicode.cldr.util.personname.PersonNameFormatter.FormatParameters; 58 import org.unicode.cldr.util.personname.PersonNameFormatter.NameObject; 59 import org.unicode.cldr.util.personname.PersonNameFormatter.NamePattern; 60 import org.unicode.cldr.util.personname.SimpleNameObject; 61 62 import com.google.common.base.Joiner; 63 import com.google.common.collect.ImmutableList; 64 import com.ibm.icu.impl.Row.R3; 65 import com.ibm.icu.impl.Utility; 66 import com.ibm.icu.impl.number.DecimalQuantity; 67 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; 68 import com.ibm.icu.lang.UCharacter; 69 import com.ibm.icu.text.BreakIterator; 70 import com.ibm.icu.text.DateFormat; 71 import com.ibm.icu.text.DateFormatSymbols; 72 import com.ibm.icu.text.DateTimePatternGenerator; 73 import com.ibm.icu.text.DecimalFormat; 74 import com.ibm.icu.text.DecimalFormatSymbols; 75 import com.ibm.icu.text.ListFormatter; 76 import com.ibm.icu.text.MessageFormat; 77 import com.ibm.icu.text.NumberFormat; 78 import com.ibm.icu.text.PluralRules; 79 import com.ibm.icu.text.PluralRules.DecimalQuantitySamples; 80 import com.ibm.icu.text.PluralRules.DecimalQuantitySamplesRange; 81 import com.ibm.icu.text.PluralRules.Operand; 82 import com.ibm.icu.text.PluralRules.SampleType; 83 import com.ibm.icu.text.SimpleDateFormat; 84 import com.ibm.icu.text.SimpleFormatter; 85 import com.ibm.icu.text.Transliterator; 86 import com.ibm.icu.text.UnicodeSet; 87 import com.ibm.icu.text.UTF16; 88 import com.ibm.icu.util.Calendar; 89 import com.ibm.icu.util.Output; 90 import com.ibm.icu.util.TimeZone; 91 import com.ibm.icu.util.ULocale; 92 93 /** 94 * Class to generate examples and help messages for the Survey tool (or console version). 95 * 96 * @author markdavis 97 */ 98 public class ExampleGenerator { 99 private static final String EXAMPLE_OF_INCORRECT = "❌ "; 100 private static final String EXAMPLE_OF_CAUTION = "⚠️ "; 101 102 private static final boolean DEBUG_EXAMPLE_GENERATOR = false; 103 104 final static boolean DEBUG_SHOW_HELP = false; 105 106 private static final CLDRConfig CONFIG = CLDRConfig.getInstance(); 107 108 private static final String ALT_STAND_ALONE = "[@alt=\"stand-alone\"]"; 109 110 private static final String EXEMPLAR_CITY_LOS_ANGELES = "//ldml/dates/timeZoneNames/zone[@type=\"America/Los_Angeles\"]/exemplarCity"; 111 112 private static final Pattern URL_PATTERN = Pattern 113 .compile("http://[\\-a-zA-Z0-9]+(\\.[\\-a-zA-Z0-9]+)*([/#][\\-a-zA-Z0-9]+)*"); 114 115 private static final SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo.getInstance(); 116 static final UnitConverter UNIT_CONVERTER = supplementalDataInfo.getUnitConverter(); 117 static final Set<String> UNITS = Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular); 118 119 public final static double NUMBER_SAMPLE = 123456.789; 120 public final static double NUMBER_SAMPLE_WHOLE = 2345; 121 122 public final static TimeZone ZONE_SAMPLE = TimeZone.getTimeZone("America/Indianapolis"); 123 public final static TimeZone GMT_ZONE_SAMPLE = TimeZone.getTimeZone("Etc/GMT"); 124 125 private static final String exampleStart = "<div class='cldr_example'>"; 126 private static final String exampleStartAuto = "<div class='cldr_example_auto' dir='auto'>"; 127 private static final String exampleStartRTL = "<div class='cldr_example_rtl' dir='rtl'>"; 128 private static final String exampleStartHeader = "<div class='cldr_example_rtl'>"; 129 private static final String exampleEnd = "</div>"; 130 private static final String startItalic = "<i>"; 131 private static final String endItalic = "</i>"; 132 private static final String startSup = "<sup>"; 133 private static final String endSup = "</sup>"; 134 private static final String backgroundAutoStart = "<span class='cldr_background_auto'>"; 135 private static final String backgroundAutoEnd = "</span>"; 136 private String backgroundStart = "<span class='cldr_substituted'>"; // overrideable 137 private String backgroundEnd = "</span>"; // overrideable 138 139 public static final String backgroundStartSymbol = "\uE234"; 140 public static final String backgroundEndSymbol = "\uE235"; 141 private static final String backgroundTempSymbol = "\uE236"; 142 private static final String exampleSeparatorSymbol = "\uE237"; 143 private static final String startItalicSymbol = "\uE238"; 144 private static final String endItalicSymbol = "\uE239"; 145 private static final String startSupSymbol = "\uE23A"; 146 private static final String endSupSymbol = "\uE23B"; 147 private static final String backgroundAutoStartSymbol = "\uE23C"; 148 private static final String backgroundAutoEndSymbol = "\uE23D"; 149 private static final String exampleStartAutoSymbol = "\uE23E"; 150 private static final String exampleStartRTLSymbol = "\uE23F"; 151 private static final String exampleStartHeaderSymbol = "\uE240"; 152 private static final String exampleEndSymbol = "\uE241"; 153 154 private static final String contextheader = "Key: " + backgroundAutoStartSymbol + "neutral" + backgroundAutoEndSymbol + ", RTL"; 155 156 public static final char TEXT_VARIANT = '\uFE0E'; 157 158 private static final UnicodeSet BIDI_MARKS = new UnicodeSet("[:Bidi_Control:]").freeze(); 159 160 public final static Date DATE_SAMPLE; 161 162 private final static Date DATE_SAMPLE2; 163 private final static Date DATE_SAMPLE3; 164 private final static Date DATE_SAMPLE4; 165 166 static { 167 Calendar calendar = Calendar.getInstance(ZONE_SAMPLE, ULocale.ENGLISH); 168 calendar.set(1999, 8, 5, 13, 25, 59); // 1999-08-05 13:25:59 169 DATE_SAMPLE = calendar.getTime(); 170 calendar.set(1999, 9, 27, 13, 25, 59); // 1999-09-27 13:25:59 171 DATE_SAMPLE2 = calendar.getTime(); 172 173 calendar.set(1999, 8, 5, 7, 0, 0); // 1999-08-5 07:00:00 174 DATE_SAMPLE3 = calendar.getTime(); 175 calendar.set(1999, 8, 5, 23, 0, 0); // 1999-08-5 23:00:00 176 DATE_SAMPLE4 = calendar.getTime(); 177 } 178 179 @SuppressWarnings("deprecation") 180 static final List<DecimalQuantity> CURRENCY_SAMPLES = ImmutableList.of( 181 DecimalQuantity_DualStorageBCD.fromExponentString("1.23"), 182 DecimalQuantity_DualStorageBCD.fromExponentString("0"), 183 DecimalQuantity_DualStorageBCD.fromExponentString("2.34"), 184 DecimalQuantity_DualStorageBCD.fromExponentString("3.45"), 185 DecimalQuantity_DualStorageBCD.fromExponentString("5.67"), 186 DecimalQuantity_DualStorageBCD.fromExponentString("1")); 187 188 public static final Pattern PARAMETER = PatternCache.get("(\\{(?:0|[1-9][0-9]*)\\})"); 189 public static final Pattern PARAMETER_SKIP0 = PatternCache.get("(\\{[1-9][0-9]*\\})"); 190 public static final Pattern ALL_DIGITS = PatternCache.get("(\\p{Nd}+(.\\p{Nd}+)?)"); 191 192 private static Calendar generatingCalendar = Calendar.getInstance(ULocale.US); 193 getDate(int year, int month, int date, int hour, int minute, int second)194 private static Date getDate(int year, int month, int date, int hour, int minute, int second) { 195 synchronized (generatingCalendar) { 196 generatingCalendar.setTimeZone(GMT_ZONE_SAMPLE); 197 generatingCalendar.set(year, month, date, hour, minute, second); 198 return generatingCalendar.getTime(); 199 } 200 } 201 202 private static Date FIRST_INTERVAL = getDate(2008, 1, 13, 5, 7, 9); 203 private static Map<String, Date> SECOND_INTERVAL = CldrUtility.asMap(new Object[][] { 204 { "G", getDate(1009, 2, 14, 17, 8, 10) }, // "G" mostly useful for calendars that have short eras, like Japanese 205 { "y", getDate(2009, 2, 14, 17, 8, 10) }, 206 { "M", getDate(2008, 2, 14, 17, 8, 10) }, 207 { "d", getDate(2008, 1, 14, 17, 8, 10) }, 208 { "a", getDate(2008, 1, 13, 17, 8, 10) }, 209 { "h", getDate(2008, 1, 13, 6, 8, 10) }, 210 { "m", getDate(2008, 1, 13, 5, 8, 10) } 211 }); 212 setCachingEnabled(boolean enabled)213 public void setCachingEnabled(boolean enabled) { 214 exCache.setCachingEnabled(enabled); 215 } 216 setCacheOnly(boolean cacheOnly)217 public void setCacheOnly(boolean cacheOnly) { 218 exCache.setCacheOnly(cacheOnly); 219 } 220 221 /** 222 * verboseErrors affects not only the verboseness of error reporting, but also, for 223 * example, whether some unit tests pass or fail. The function setVerboseErrors 224 * can be used to modify it. It must be initialized here to false, otherwise 225 * cldr-unittest TestAll.java fails. Reference: https://unicode.org/cldr/trac/ticket/12025 226 */ 227 private boolean verboseErrors = false; 228 229 private Calendar calendar = Calendar.getInstance(ZONE_SAMPLE, ULocale.ENGLISH); 230 231 private CLDRFile cldrFile; 232 233 private CLDRFile englishFile; 234 private BestMinimalPairSamples bestMinimalPairSamples = null; 235 236 private ExampleCache exCache = new ExampleCache(); 237 238 private ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder(); 239 240 private PluralInfo pluralInfo; 241 242 private GrammarInfo grammarInfo; 243 244 private PluralSamples patternExamples; 245 246 private Map<String, String> subdivisionIdToName; 247 248 private String creationTime = null; // only used if DEBUG_EXAMPLE_GENERATOR 249 250 private IntervalFormat intervalFormat = new IntervalFormat(); 251 252 private PathDescription pathDescription; 253 254 /** 255 * True if this ExampleGenerator is especially for generating "English" examples, 256 * false if it is for generating "native" examples. 257 */ 258 private boolean typeIsEnglish; 259 260 /** 261 * True if this ExampleGenerator is for RTL locale. 262 */ 263 private boolean isRTL; 264 265 HelpMessages helpMessages; 266 getCldrFile()267 public CLDRFile getCldrFile() { 268 return cldrFile; 269 } 270 271 /** 272 * For this (locale-specific) ExampleGenerator, clear the cached examples for 273 * any paths whose examples might depend on the winning value of the given path, 274 * since the winning value of the given path has changed. 275 * 276 * @param xpath the path whose winning value has changed 277 * 278 * Called by TestCache.updateExampleGeneratorCache 279 */ updateCache(String xpath)280 public void updateCache(String xpath) { 281 exCache.update(xpath); 282 } 283 284 /** 285 * For getting the end of the "background" style. Default is "</span>". It is 286 * used in composing patterns, so it can show the part that corresponds to the 287 * value. 288 * 289 * @return 290 */ getBackgroundEnd()291 public String getBackgroundEnd() { 292 return backgroundEnd; 293 } 294 295 /** 296 * For setting the end of the "background" style. Default is "</span>". It is 297 * used in composing patterns, so it can show the part that corresponds to the 298 * value. 299 * 300 * @return 301 */ setBackgroundEnd(String backgroundEnd)302 public void setBackgroundEnd(String backgroundEnd) { 303 this.backgroundEnd = backgroundEnd; 304 } 305 306 /** 307 * For getting the "background" style. Default is "<span 308 * style='background-color: gray'>". It is used in composing patterns, so it 309 * can show the part that corresponds to the value. 310 * 311 * @return 312 */ getBackgroundStart()313 public String getBackgroundStart() { 314 return backgroundStart; 315 } 316 317 /** 318 * For setting the "background" style. Default is "<span 319 * style='background-color: gray'>". It is used in composing patterns, so it 320 * can show the part that corresponds to the value. 321 * 322 * @return 323 */ setBackgroundStart(String backgroundStart)324 public void setBackgroundStart(String backgroundStart) { 325 this.backgroundStart = backgroundStart; 326 } 327 328 /** 329 * Set the verbosity level of internal errors. 330 * For example, setVerboseErrors(true) will cause 331 * full stack traces to be shown in some cases. 332 */ setVerboseErrors(boolean verbosity)333 public void setVerboseErrors(boolean verbosity) { 334 this.verboseErrors = verbosity; 335 } 336 337 /** 338 * Create an Example Generator. If this is shared across threads, it must be synchronized. 339 * 340 * @param resolvedCldrFile 341 * @param englishFile 342 * @param supplementalDataDirectory 343 */ ExampleGenerator(CLDRFile resolvedCldrFile, CLDRFile englishFile, String supplementalDataDirectory)344 public ExampleGenerator(CLDRFile resolvedCldrFile, CLDRFile englishFile, String supplementalDataDirectory) { 345 if (!resolvedCldrFile.isResolved()) { 346 throw new IllegalArgumentException("CLDRFile must be resolved"); 347 } 348 if (!englishFile.isResolved()) { 349 throw new IllegalArgumentException("English CLDRFile must be resolved"); 350 } 351 cldrFile = resolvedCldrFile; 352 final String localeId = cldrFile.getLocaleID(); 353 subdivisionIdToName = EmojiSubdivisionNames.getSubdivisionIdToName(localeId); 354 pluralInfo = supplementalDataInfo.getPlurals(PluralType.cardinal, localeId); 355 grammarInfo = supplementalDataInfo.getGrammarInfo(localeId); // getGrammarInfo can return null 356 this.englishFile = englishFile; 357 this.typeIsEnglish = (resolvedCldrFile == englishFile); 358 icuServiceBuilder.setCldrFile(cldrFile); 359 360 bestMinimalPairSamples = new BestMinimalPairSamples(cldrFile, icuServiceBuilder, false); 361 362 String characterOrder = cldrFile.getStringValue("//ldml/layout/orientation/characterOrder"); 363 this.isRTL = (characterOrder != null && characterOrder.equals("right-to-left")); 364 365 if (DEBUG_EXAMPLE_GENERATOR) { 366 creationTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Calendar.getInstance().getTime()); 367 System.out.println(" Created new ExampleGenerator for loc " + localeId + " at " + creationTime); 368 } 369 } 370 371 /** 372 * Get an example string, in html, if there is one for this path, 373 * otherwise null. For use in the survey tool, an example might be returned 374 * *even* if there is no value in the locale. For example, the locale might 375 * have a path that English doesn't, but you want to return the best English 376 * example. <br> 377 * The result is valid HTML. 378 * 379 * If generating examples for an inheritance marker, use the "real" inherited value 380 * to generate from. Do this BEFORE accessing the cache, which doesn't use INHERITANCE_MARKER. 381 * 382 * @param xpath the path; e.g., "//ldml/dates/timeZoneNames/fallbackFormat" 383 * @param value the value; e.g., "{1} [{0}]"; not necessarily the winning value 384 * @return the example HTML, or null 385 */ getExampleHtml(String xpath, String value)386 public String getExampleHtml(String xpath, String value) { 387 if (value == null || xpath == null || xpath.endsWith("/alias")) { 388 return null; 389 } 390 String result = null; 391 try { 392 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 393 value = cldrFile.getBaileyValue(xpath, null, null); 394 if (value == null) { 395 /* 396 * This can happen for some paths, such as 397 * //ldml/dates/timeZoneNames/metazone[@type="Mawson"]/short/daylight 398 */ 399 return null; 400 } 401 } 402 ExampleCache.ExampleCacheItem cacheItem = exCache.new ExampleCacheItem(xpath, value); 403 result = cacheItem.getExample(); 404 if (result != null) { 405 return result; 406 } 407 result = constructExampleHtml(xpath, value); 408 cacheItem.putExample(result); 409 } catch (RuntimeException e) { 410 e.printStackTrace(); 411 String unchained = verboseErrors ? ("<br>" + finalizeBackground(unchainException(e))) : ""; 412 result = "<i>Parsing error. " + finalizeBackground(e.getMessage()) + "</i>" + unchained; 413 } 414 return result; 415 } 416 417 /** 418 * Do the main work of getExampleHtml given that the result was not 419 * found in the cache. 420 * 421 * @param xpath the path; e.g., "//ldml/dates/timeZoneNames/fallbackFormat" 422 * @param value the value; e.g., "{1} [{0}]"; not necessarily the winning value 423 * @return the example HTML, or null 424 */ constructExampleHtml(String xpath, String value)425 private String constructExampleHtml(String xpath, String value) { 426 String result = null; 427 boolean showContexts = isRTL || BIDI_MARKS.containsSome(value); // only used for certain example types 428 /* 429 * Need getInstance, not getFrozenInstance here: some functions such as handleNumberSymbol 430 * expect to call functions like parts.addRelative which throw exceptions if parts is frozen. 431 */ 432 XPathParts parts = XPathParts.getFrozenInstance(xpath).cloneAsThawed(); 433 if (parts.contains("dateRangePattern")) { // {0} - {1} 434 result = handleDateRangePattern(value); 435 } else if (parts.contains("timeZoneNames")) { 436 result = handleTimeZoneName(parts, value); 437 } else if (parts.contains("localeDisplayNames")) { 438 result = handleDisplayNames(xpath, parts, value); 439 } else if (parts.contains("currency")) { 440 result = handleCurrency(xpath, parts, value); 441 } else if (parts.contains("dayPeriods")) { 442 result = handleDayPeriod(parts, value); 443 } else if (parts.contains("pattern") || parts.contains("dateFormatItem")) { 444 if (parts.contains("calendar")) { 445 result = handleDateFormatItem(xpath, value, showContexts); 446 } else if (parts.contains("miscPatterns")) { 447 result = handleMiscPatterns(parts, value); 448 } else if (parts.contains("numbers")) { 449 if (parts.contains("currencyFormat")) { 450 result = handleCurrencyFormat(parts, value, showContexts); 451 } else { 452 result = handleDecimalFormat(parts, value, showContexts); 453 } 454 } 455 } else if (parts.getElement(2).contains("symbols")) { 456 result = handleNumberSymbol(parts, value); 457 } else if (parts.contains("defaultNumberingSystem") || parts.contains("otherNumberingSystems")) { 458 result = handleNumberingSystem(value); 459 } else if (parts.contains("currencyFormats") && parts.contains("unitPattern")) { 460 result = formatCountValue(xpath, parts, value); 461 } else if (parts.getElement(-1).equals("compoundUnitPattern")) { 462 result = handleCompoundUnit(parts); 463 } else if (parts.getElement(-1).equals("compoundUnitPattern1") 464 || parts.getElement(-1).equals("unitPrefixPattern")) { 465 result = handleCompoundUnit1(parts, value); 466 } else if (parts.getElement(-1).equals("unitPattern")) { 467 result = handleFormatUnit(parts, value); 468 } else if (parts.getElement(-1).equals("perUnitPattern")) { 469 result = handleFormatPerUnit(parts, value); 470 } else if (parts.getElement(-2).equals("minimalPairs")) { 471 result = handleMinimalPairs(parts, value); 472 } else if (parts.getElement(-1).equals("durationUnitPattern")) { 473 result = handleDurationUnit(value); 474 } else if (parts.contains("intervalFormats")) { 475 result = handleIntervalFormats(parts, value); 476 } else if (parts.getElement(1).equals("delimiters")) { 477 result = handleDelimiters(parts, xpath, value); 478 } else if (parts.getElement(1).equals("listPatterns")) { 479 result = handleListPatterns(parts, value); 480 } else if (parts.getElement(2).equals("ellipsis")) { 481 result = handleEllipsis(parts.getAttributeValue(-1, "type"), value); 482 } else if (parts.getElement(-1).equals("monthPattern")) { 483 result = handleMonthPatterns(parts, value); 484 } else if (parts.getElement(-1).equals("appendItem")) { 485 result = handleAppendItems(parts, value); 486 } else if (parts.getElement(-1).equals("annotation")) { 487 result = handleAnnotationName(parts, value); 488 } else if (parts.getElement(-1).equals("characterLabel")) { 489 result = handleLabel(parts, value); 490 } else if (parts.getElement(-1).equals("characterLabelPattern")) { 491 result = handleLabelPattern(parts, value); 492 } else if (parts.getElement(1).equals("personNames")) { 493 result = handlePersonName(parts, value); 494 } 495 if (result != null) { 496 // if (!typeIsEnglish) { 497 // result = addTransliteration(result, value); 498 // } 499 result = finalizeBackground(result); 500 } 501 return result; 502 } 503 504 /** 505 * Holds a map and an object that are relatively expensive to build, 506 * so we don't want to do that on each call. 507 * TODO clean up the synchronization model. 508 */ 509 private class PersonNamesCache implements ExampleCache.ClearableCache { 510 Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNames = null; 511 PersonNameFormatter personNameFormatter = null; 512 @Override clear()513 public void clear() { 514 sampleNames = null; 515 personNameFormatter = null; 516 } getSampleNames(CLDRFile cldrFile)517 Map<PersonNameFormatter.SampleType, SimpleNameObject> getSampleNames(CLDRFile cldrFile) { 518 synchronized (this) { 519 if (sampleNames == null) { 520 sampleNames = PersonNameFormatter.loadSampleNames(cldrFile); 521 } 522 return sampleNames; 523 } 524 } getPersonNameFormatter(CLDRFile cldrFile)525 PersonNameFormatter getPersonNameFormatter(CLDRFile cldrFile) { 526 synchronized (this) { 527 if (personNameFormatter == null) { 528 personNameFormatter = new PersonNameFormatter(cldrFile); 529 } 530 return personNameFormatter; 531 } 532 } 533 @Override toString()534 public String toString() { 535 return "[" + (sampleNames == null ? "" : Joiner.on('\n').join(sampleNames.entrySet())) 536 + ", " + (personNameFormatter == null ? "" : personNameFormatter.toString()) 537 + "]"; 538 } 539 } 540 541 /** 542 * Register the cache, so that it gets cleared when any of the paths change 543 */ 544 PersonNamesCache personNamesCache = exCache.registerCache(new PersonNamesCache(), 545 "//ldml/personNames/sampleName[@item=\"*\"]/nameField[@type=\"*\"]", 546 "//ldml/personNames/initialPattern[@type=\"*\"]"); 547 handlePersonName(XPathParts parts, String value)548 private String handlePersonName(XPathParts parts, String value) { 549 //ldml/personNames/personName[@order="givenFirst"][@length="long"][@usage="addressing"][@style="formal"]/namePattern => {prefix} {surname} 550 String debugState = "start"; 551 try { 552 FormatParameters formatParameters = new FormatParameters( 553 PersonNameFormatter.Order.from(parts.getAttributeValue(2, "order")), 554 PersonNameFormatter.Length.from(parts.getAttributeValue(2, "length")), 555 PersonNameFormatter.Usage.from(parts.getAttributeValue(2, "usage")), 556 PersonNameFormatter.Formality.from(parts.getAttributeValue(2, "formality"))); 557 558 List<String> examples = null; 559 final CLDRFile cldrFile2 = getCldrFile(); 560 switch(parts.getElement(2)) { 561 case "nameOrderLocales": 562 examples = new ArrayList<>(); 563 for (String localeId : PersonNameFormatter.SPLIT_SPACE.split(value)) { 564 final String name = localeId.equals("und") ? "«any other»" : cldrFile2.getName(localeId); 565 examples.add(localeId + " = " + name); 566 } 567 break; 568 case "initialPattern": 569 return null; 570 case "sampleName": 571 return null; 572 case "personName": 573 examples = new ArrayList<>(); 574 Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNames = personNamesCache.getSampleNames(cldrFile2); 575 PersonNameFormatter personNameFormatter = personNamesCache.getPersonNameFormatter(cldrFile2); 576 577 // We might need the alt, however: String alt = parts.getAttributeValue(-1, "alt"); 578 579 for (NameObject sampleNameObject1 : sampleNames.values()) { 580 NamePattern namePattern = NamePattern.from(0, value); 581 debugState = "<NamePattern.from: " + namePattern; 582 final FallbackFormatter fallbackInfo = personNameFormatter.getFallbackInfo(); 583 debugState = "<getFallbackInfo: " + fallbackInfo; 584 String result = namePattern.format(sampleNameObject1, formatParameters, fallbackInfo); 585 debugState = "<namePattern.format: " + result; 586 examples.add(result); 587 } 588 break; 589 } 590 return formatExampleList(examples); 591 } catch (Exception e) { 592 StringBuffer stackTrace = null; 593 try (StringWriter sw = new StringWriter(); 594 PrintWriter p = new PrintWriter(sw)) { 595 e.printStackTrace(p); 596 stackTrace = sw.getBuffer(); 597 } catch (Exception e2) { 598 stackTrace = new StringBuffer("internal error"); 599 } 600 return "Internal error: " + e.getMessage() + "\n" + debugState + "\n" + stackTrace; 601 } 602 } 603 handleLabelPattern(XPathParts parts, String value)604 private String handleLabelPattern(XPathParts parts, String value) { 605 switch (parts.getAttributeValue(-1, "type")) { 606 case "category-list": 607 List<String> examples = new ArrayList<>(); 608 CLDRFile cfile = getCldrFile(); 609 SimpleFormatter initialPattern = SimpleFormatter.compile(setBackground(value)); 610 String path = CLDRFile.getKey(CLDRFile.TERRITORY_NAME, "FR"); 611 String regionName = cfile.getStringValue(path); 612 String flagName = cfile.getStringValue("//ldml/characterLabels/characterLabel[@type=\"flag\"]"); 613 examples.add(invertBackground(EmojiConstants.getEmojiFromRegionCodes("FR") 614 + " ⇒ " + initialPattern.format(flagName, regionName))); 615 return formatExampleList(examples); 616 default: 617 return null; 618 } 619 } 620 handleLabel(XPathParts parts, String value)621 private String handleLabel(XPathParts parts, String value) { 622 // "//ldml/characterLabels/characterLabel[@type=\"" + typeAttributeValue + "\"]" 623 switch (parts.getAttributeValue(-1, "type")) { 624 case "flag": { 625 String value2 = backgroundStartSymbol + value + backgroundEndSymbol; 626 CLDRFile cfile = getCldrFile(); 627 List<String> examples = new ArrayList<>(); 628 SimpleFormatter initialPattern = SimpleFormatter.compile(cfile.getStringValue("//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 629 addFlag(value2, "FR", cfile, initialPattern, examples); 630 addFlag(value2, "CN", cfile, initialPattern, examples); 631 addSubdivisionFlag(value2, "gbeng", initialPattern, examples); 632 addSubdivisionFlag(value2, "gbsct", initialPattern, examples); 633 addSubdivisionFlag(value2, "gbwls", initialPattern, examples); 634 return formatExampleList(examples); 635 } 636 case "keycap": { 637 String value2 = backgroundStartSymbol + value + backgroundEndSymbol; 638 List<String> examples = new ArrayList<>(); 639 CLDRFile cfile = getCldrFile(); 640 SimpleFormatter initialPattern = SimpleFormatter.compile(cfile.getStringValue("//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 641 examples.add(invertBackground(initialPattern.format(value2, "1"))); 642 examples.add(invertBackground(initialPattern.format(value2, "10"))); 643 examples.add(invertBackground(initialPattern.format(value2, "#"))); 644 return formatExampleList(examples); 645 } 646 default: 647 return null; 648 } 649 } 650 addFlag(String value2, String isoRegionCode, CLDRFile cfile, SimpleFormatter initialPattern, List<String> examples)651 private void addFlag(String value2, String isoRegionCode, CLDRFile cfile, SimpleFormatter initialPattern, List<String> examples) { 652 String path = CLDRFile.getKey(CLDRFile.TERRITORY_NAME, isoRegionCode); 653 String regionName = cfile.getStringValue(path); 654 examples.add(invertBackground(EmojiConstants.getEmojiFromRegionCodes(isoRegionCode) 655 + " ⇒ " + initialPattern.format(value2, regionName))); 656 } 657 addSubdivisionFlag(String value2, String isoSubdivisionCode, SimpleFormatter initialPattern, List<String> examples)658 private void addSubdivisionFlag(String value2, String isoSubdivisionCode, SimpleFormatter initialPattern, List<String> examples) { 659 String subdivisionName = subdivisionIdToName.get(isoSubdivisionCode); 660 if (subdivisionName == null) { 661 subdivisionName = isoSubdivisionCode; 662 } 663 examples.add(invertBackground(EmojiConstants.getEmojiFromSubdivisionCodes(isoSubdivisionCode) 664 + " ⇒ " + initialPattern.format(value2, subdivisionName))); 665 } 666 handleAnnotationName(XPathParts parts, String value)667 private String handleAnnotationName(XPathParts parts, String value) { 668 //ldml/annotations/annotation[@cp=""][@type="tts"] 669 // skip anything but the name 670 if (!"tts".equals(parts.getAttributeValue(-1, "type"))) { 671 return null; 672 } 673 String cp = parts.getAttributeValue(-1, "cp"); 674 if (cp == null || cp.isEmpty()) { 675 return null; 676 } 677 Set<String> examples = new LinkedHashSet<>(); 678 int first = cp.codePointAt(0); 679 switch(first) { 680 case 0x1F46A: // U+1F46A FAMILY 681 examples.add(formatGroup(value, "", "", "", "", "")); 682 examples.add(formatGroup(value, "", "", "", "")); 683 break; 684 case 0x1F48F: // U+1F48F KISS 685 examples.add(formatGroup(value, "❤️", "", "")); 686 examples.add(formatGroup(value, "❤️", "", "")); 687 break; 688 case 0x1F491: // U+1F491 COUPLE WITH HEART 689 examples.add(formatGroup(value, "❤️", "", "")); 690 examples.add(formatGroup(value, "❤️", "", "")); 691 break; 692 default: 693 boolean isSkin = EmojiConstants.MODIFIERS.contains(first); 694 if (isSkin || EmojiConstants.HAIR.contains(first)) { 695 String value2 = backgroundStartSymbol + value + backgroundEndSymbol; 696 CLDRFile cfile = getCldrFile(); 697 String skin = ""; 698 String hair = ""; 699 String skinName = getEmojiName(cfile, skin); 700 String hairName = getEmojiName(cfile, hair); 701 if (hairName == null) { 702 hair = "[missing]"; 703 } 704 SimpleFormatter initialPattern = SimpleFormatter.compile(cfile.getStringValue("//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 705 SimpleFormatter listPattern = SimpleFormatter.compile(cfile.getStringValue("//ldml/listPatterns/listPattern[@type=\"unit-short\"]/listPatternPart[@type=\"2\"]")); 706 707 hair = EmojiConstants.JOINER_STRING + hair; 708 formatPeople(cfile, first, isSkin, value2, "", skin, skinName, hair, hairName, initialPattern, listPattern, examples); 709 formatPeople(cfile, first, isSkin, value2, "", skin, skinName, hair, hairName, initialPattern, listPattern, examples); 710 } 711 break; 712 } 713 return formatExampleList(examples); 714 } 715 getEmojiName(CLDRFile cfile, String skin)716 private String getEmojiName(CLDRFile cfile, String skin) { 717 return cfile.getStringValue("//ldml/annotations/annotation[@cp=\"" + skin + "\"][@type=\"tts\"]"); 718 } 719 720 //ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"] formatGroup(String value, String sourceEmoji, String... components)721 private String formatGroup(String value, String sourceEmoji, String... components) { 722 CLDRFile cfile = getCldrFile(); 723 SimpleFormatter initialPattern = SimpleFormatter.compile(cfile.getStringValue("//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 724 String value2 = backgroundEndSymbol + value + backgroundStartSymbol; 725 String[] names = new String[components.length]; 726 int i = 0; 727 for (String component : components) { 728 names[i++] = getEmojiName(cfile, component); 729 } 730 return backgroundStartSymbol + sourceEmoji + " ⇒ " + initialPattern.format(value2, 731 longListPatternExample(EmojiConstants.COMPOSED_NAME_LIST.getPath(), "n/a", "n/a2", names)); 732 } 733 formatPeople(CLDRFile cfile, int first, boolean isSkin, String value2, String person, String skin, String skinName, String hair, String hairName, SimpleFormatter initialPattern, SimpleFormatter listPattern, Collection<String> examples)734 private void formatPeople(CLDRFile cfile, int first, boolean isSkin, String value2, String person, String skin, String skinName, 735 String hair, String hairName, SimpleFormatter initialPattern, SimpleFormatter listPattern, Collection<String> examples) { 736 String cp; 737 String personName = getEmojiName(cfile, person); 738 StringBuilder emoji = new StringBuilder(person).appendCodePoint(first); 739 cp = UTF16.valueOf(first); 740 cp = isSkin ? cp : EmojiConstants.JOINER_STRING + cp; 741 examples.add(person + cp + " ⇒ " + invertBackground(initialPattern.format(personName,value2))); 742 emoji.setLength(0); 743 emoji.append(personName); 744 if (isSkin) { 745 skinName = value2; 746 skin = cp; 747 } else { 748 hairName = value2; 749 hair = cp; 750 } 751 examples.add(person + skin + hair + " ⇒ " + invertBackground(listPattern.format(initialPattern.format(personName, skinName), hairName))); 752 } 753 handleDayPeriod(XPathParts parts, String value)754 private String handleDayPeriod(XPathParts parts, String value) { 755 //ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="morning1"] 756 //ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="stand-alone"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="morning1"] 757 List<String> examples = new ArrayList<>(); 758 final String dayPeriodType = parts.getAttributeValue(5, "type"); 759 if (dayPeriodType == null) { 760 return null; // formerly happened for some "/alias" paths 761 } 762 org.unicode.cldr.util.DayPeriodInfo.Type aType = dayPeriodType.equals("format") ? DayPeriodInfo.Type.format : DayPeriodInfo.Type.selection; 763 DayPeriodInfo dayPeriodInfo = supplementalDataInfo.getDayPeriods(aType, cldrFile.getLocaleID()); 764 String periodString = parts.getAttributeValue(-1, "type"); 765 if (periodString == null) { 766 return null; // formerly happened for some "/alias" paths 767 } 768 DayPeriod dayPeriod = DayPeriod.valueOf(periodString); 769 String periods = dayPeriodInfo.toString(dayPeriod); 770 examples.add(periods); 771 if ("format".equals(dayPeriodType)) { 772 if (value == null) { 773 value = "�"; 774 } 775 R3<Integer, Integer, Boolean> info = dayPeriodInfo.getFirstDayPeriodInfo(dayPeriod); 776 if (info != null) { 777 int time = (((info.get0() + info.get1()) % DayPeriodInfo.DAY_LIMIT) / 2); 778 String timeFormatString = icuServiceBuilder.formatDayPeriod(time, backgroundStartSymbol + value + backgroundEndSymbol); 779 examples.add(invertBackground(timeFormatString)); 780 } 781 } 782 return formatExampleList(examples.toArray(new String[examples.size()])); 783 } 784 handleMinimalPairs(XPathParts parts, String minimalPattern)785 private String handleMinimalPairs(XPathParts parts, String minimalPattern) { 786 List<String> examples = new ArrayList<>(); 787 788 Output<String> output = new Output<>(); 789 String count = null; 790 String otherCount = null; 791 String sample = null; 792 String sampleBad = null; 793 String locale = getCldrFile().getLocaleID(); 794 795 switch(parts.getElement(-1)) { 796 797 case "ordinalMinimalPairs": //ldml/numbers/minimalPairs/ordinalMinimalPairs[@count="one"] 798 count = parts.getAttributeValue(-1, "ordinal"); 799 sample = bestMinimalPairSamples.getPluralOrOrdinalSample(PluralType.ordinal, count); // Pick a unit that exhibits the most variation 800 otherCount = getOtherCount(locale, PluralType.ordinal, count); 801 sampleBad = bestMinimalPairSamples.getPluralOrOrdinalSample(PluralType.ordinal, otherCount); // Pick a unit that exhibits the most variation 802 break; 803 804 case "pluralMinimalPairs": //ldml/numbers/minimalPairs/pluralMinimalPairs[@count="one"] 805 count = parts.getAttributeValue(-1, "count"); 806 sample = bestMinimalPairSamples.getPluralOrOrdinalSample(PluralType.cardinal, count); // Pick a unit that exhibits the most variation 807 otherCount = getOtherCount(locale, PluralType.cardinal, count); 808 sampleBad = bestMinimalPairSamples.getPluralOrOrdinalSample(PluralType.cardinal, otherCount); // Pick a unit that exhibits the most variation 809 break; 810 811 case "caseMinimalPairs": //ldml/numbers/minimalPairs/caseMinimalPairs[@case="accusative"] 812 String gCase = parts.getAttributeValue(-1, "case"); 813 sample = bestMinimalPairSamples.getBestUnitWithCase(gCase, output); // Pick a unit that exhibits the most variation 814 sampleBad = getOtherCase(bestMinimalPairSamples, locale, gCase, sample); 815 break; 816 817 case "genderMinimalPairs": //ldml/numbers/minimalPairs/genderMinimalPairs[@gender="feminine"] 818 String gender = parts.getAttributeValue(-1, "gender"); 819 sample = bestMinimalPairSamples.getBestUnitWithGender(gender, output); 820 String otherGender = getOtherGender(locale, gender); 821 sampleBad = bestMinimalPairSamples.getBestUnitWithGender(otherGender, output); 822 break; 823 default: 824 return null; 825 } 826 String formattedUnit = format(minimalPattern, backgroundStartSymbol + sample + backgroundEndSymbol); 827 examples.add(formattedUnit); 828 if (sampleBad == null) { 829 sampleBad = "n/a"; 830 } 831 formattedUnit = format(minimalPattern, backgroundStartSymbol + sampleBad + backgroundEndSymbol); 832 examples.add(EXAMPLE_OF_INCORRECT + formattedUnit); 833 return formatExampleList(examples); 834 } 835 getOtherGender(String locale, String gender)836 private String getOtherGender(String locale, String gender) { 837 Collection<String> unitGenders = grammarInfo.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalGender, GrammaticalScope.units); 838 for (String otherGender : unitGenders) { 839 if (!gender.equals(otherGender)) { 840 return otherGender; 841 } 842 } 843 return null; 844 } 845 getOtherCase(BestMinimalPairSamples bestMinimalPairSamples2, String locale, String gCase, String sample)846 private String getOtherCase(BestMinimalPairSamples bestMinimalPairSamples2, String locale, String gCase, String sample) { 847 Collection<String> unitCases = grammarInfo.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalCase, GrammaticalScope.units); 848 Output<String> output = new Output<>(); 849 for (String otherCase : unitCases) { 850 String sampleBad = bestMinimalPairSamples.getBestUnitWithCase(otherCase, output); // Pick a unit that exhibits the most variation 851 if (!sampleBad.equals(sample)) { 852 return sampleBad; 853 } 854 } 855 return null; 856 } 857 getOtherCount(String locale, PluralType ordinal, String count)858 private static String getOtherCount(String locale, PluralType ordinal, String count) { 859 String otherCount = null; 860 if (!Objects.equals(count, "other")) { 861 otherCount = "other"; 862 } else { 863 PluralInfo rules = SupplementalDataInfo.getInstance().getPlurals(ordinal, locale); 864 Set<String> counts = rules.getAdjustedCountStrings(); 865 for (String tryCount : counts) { 866 if (!tryCount.equals("other")) { 867 otherCount = tryCount; 868 break; 869 } 870 } 871 } 872 return otherCount; 873 } 874 getUnitLength(XPathParts parts)875 private UnitLength getUnitLength(XPathParts parts) { 876 return UnitLength.valueOf(parts.getAttributeValue(-3, "type").toUpperCase(Locale.ENGLISH)); 877 } 878 handleFormatUnit(XPathParts parts, String unitPattern)879 private String handleFormatUnit(XPathParts parts, String unitPattern) { 880 // Sample: //ldml/units/unitLength[@type="long"]/unit[@type="duration-day"]/unitPattern[@count="one"][@case="accusative"] 881 882 String count = parts.getAttributeValue(-1, "count"); 883 List<String> examples = new ArrayList<>(); 884 /* 885 * PluralRules.FixedDecimal is deprecated, but deprecated in ICU is 886 * also used to mark internal methods (which are OK for us to use in CLDR). 887 */ 888 @SuppressWarnings("deprecation") 889 DecimalQuantity amount = getBest(Count.valueOf(count)); 890 if (amount == null) { 891 return null; 892 } 893 DecimalFormat numberFormat = null; 894 String formattedAmount = null; 895 numberFormat = icuServiceBuilder.getNumberFormat(1); 896 formattedAmount = numberFormat.format(amount.toBigDecimal()); 897 examples.add(format(unitPattern, backgroundStartSymbol + formattedAmount + backgroundEndSymbol)); 898 899 if (parts.getElement(-2).equals("unit")) { 900 String longUnitId = parts.getAttributeValue(-2, "type"); 901 final String shortUnitId = UNIT_CONVERTER.getShortId(longUnitId); 902 if (UnitConverter.HACK_SKIP_UNIT_NAMES.contains(shortUnitId)) { 903 return null; 904 } 905 if (unitPattern != null) { 906 String gCase = parts.getAttributeValue(-1, "case"); 907 if (gCase == null) { 908 gCase = GrammaticalFeature.grammaticalCase.getDefault(null); 909 } 910 Collection<String> unitCaseInfo = null; 911 if (grammarInfo != null) { 912 unitCaseInfo = grammarInfo.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalCase, GrammaticalScope.units); 913 } 914 String minimalPattern = cldrFile.getStringValue("//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\"" + gCase + "\"]"); 915 if (minimalPattern != null && numberFormat != null) { 916 String composed = format(unitPattern, backgroundStartSymbol + formattedAmount + backgroundEndSymbol); 917 examples.add(backgroundStartSymbol + format(minimalPattern, backgroundEndSymbol + composed + backgroundStartSymbol) + backgroundEndSymbol); 918 // get contrasting case 919 if (unitCaseInfo != null && !unitCaseInfo.isEmpty()) { 920 String constrastingCase = getConstrastingCase(unitPattern, gCase, unitCaseInfo, parts); 921 if (constrastingCase != null) { 922 minimalPattern = cldrFile.getStringValue("//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\"" + constrastingCase + "\"]"); 923 composed = format(unitPattern, backgroundStartSymbol + formattedAmount + backgroundEndSymbol); 924 examples.add(EXAMPLE_OF_INCORRECT + backgroundStartSymbol + format(minimalPattern, backgroundEndSymbol + composed + backgroundStartSymbol) + backgroundEndSymbol); 925 } 926 } else { 927 examples.add(EXAMPLE_OF_CAUTION + "️No Case Minimal Pair available yet️"); 928 } 929 } 930 } 931 } 932 return formatExampleList(examples); 933 } 934 getConstrastingCase(String unitPattern, String gCase, Collection<String> unitCaseInfo, XPathParts parts)935 private String getConstrastingCase(String unitPattern, String gCase, Collection<String> unitCaseInfo, XPathParts parts) { 936 for (String otherCase : unitCaseInfo) { 937 if (otherCase.equals(gCase)) { 938 continue; 939 } 940 parts.putAttributeValue(-1, "case", "nominative".equals(otherCase) ? null : otherCase); 941 String otherValue = cldrFile.getStringValue(parts.toString()); 942 if (otherValue != null && !otherValue.equals(unitPattern)) { 943 return otherCase; 944 } 945 } 946 return null; 947 } 948 handleFormatPerUnit(XPathParts parts, String value)949 private String handleFormatPerUnit(XPathParts parts, String value) { 950 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(1); 951 return format(value, backgroundStartSymbol + numberFormat.format(1) + backgroundEndSymbol); 952 } 953 handleCompoundUnit(XPathParts parts)954 public String handleCompoundUnit(XPathParts parts) { 955 UnitLength unitLength = getUnitLength(parts); 956 String compoundType = parts.getAttributeValue(-2, "type"); 957 Count count = Count.valueOf(CldrUtility.ifNull(parts.getAttributeValue(-1, "count"), "other")); 958 return handleCompoundUnit(unitLength, compoundType, count); 959 } 960 961 @SuppressWarnings("deprecation") handleCompoundUnit(UnitLength unitLength, String compoundType, Count count)962 public String handleCompoundUnit(UnitLength unitLength, String compoundType, Count count) { 963 /** 964 * <units> 965 <unitLength type="long"> 966 <alias source="locale" path="../unitLength[@type='short']"/> 967 </unitLength> 968 <unitLength type="short"> 969 <compoundUnit type="per"> 970 <unitPattern count="other">{0}/{1}</unitPattern> 971 </compoundUnit> 972 973 * <compoundUnit type="per"> 974 <unitPattern count="one">{0}/{1}</unitPattern> 975 <unitPattern count="other">{0}/{1}</unitPattern> 976 </compoundUnit> 977 <unit type="length-m"> 978 <unitPattern count="one">{0} meter</unitPattern> 979 <unitPattern count="other">{0} meters</unitPattern> 980 </unit> 981 982 */ 983 984 // we want to get a number that works for the count passed in. 985 DecimalQuantity amount = getBest(count); 986 if (amount == null) { 987 return "n/a"; 988 } 989 DecimalQuantity oneValue = DecimalQuantity_DualStorageBCD.fromExponentString("1"); 990 991 String unit1mid; 992 String unit2mid; 993 switch (compoundType) { 994 default: 995 return "n/a"; 996 case "per": 997 unit1mid = getFormattedUnit("length-meter", unitLength, amount); 998 unit2mid = getFormattedUnit("duration-second", unitLength, oneValue, ""); 999 break; 1000 case "times": 1001 unit1mid = getFormattedUnit("force-newton", unitLength, oneValue, icuServiceBuilder.getNumberFormat(1).format(amount.toBigDecimal())); 1002 unit2mid = getFormattedUnit("length-meter", unitLength, amount, ""); 1003 break; 1004 } 1005 String unit1 = backgroundStartSymbol + unit1mid.trim() + backgroundEndSymbol; 1006 String unit2 = backgroundStartSymbol + unit2mid.trim() + backgroundEndSymbol; 1007 1008 String form = this.pluralInfo.getPluralRules().select(amount); 1009 // we rebuild a path, because we may have changed it. 1010 String perPath = makeCompoundUnitPath(unitLength, compoundType, "compoundUnitPattern"); 1011 return format(getValueFromFormat(perPath, form), unit1, unit2); 1012 } 1013 handleCompoundUnit1(XPathParts parts, String compoundPattern)1014 public String handleCompoundUnit1(XPathParts parts, String compoundPattern) { 1015 UnitLength unitLength = getUnitLength(parts); 1016 String pathCount = parts.getAttributeValue(-1, "count"); 1017 if (pathCount == null) { 1018 return handleCompoundUnit1Name(unitLength, compoundPattern); 1019 } else { 1020 return handleCompoundUnit1(unitLength, Count.valueOf(pathCount), compoundPattern); 1021 } 1022 } 1023 handleCompoundUnit1Name(UnitLength unitLength, String compoundPattern)1024 private String handleCompoundUnit1Name(UnitLength unitLength, String compoundPattern) { 1025 String pathFormat = "//ldml/units/unitLength" + unitLength.typeString + "/unit[@type=\"{0}\"]/displayName"; 1026 1027 String meterFormat = getValueFromFormat(pathFormat, "length-meter"); 1028 1029 String modFormat = combinePrefix(meterFormat, compoundPattern, unitLength == UnitLength.LONG); 1030 1031 return removeEmptyRuns(modFormat); 1032 } 1033 handleCompoundUnit1(UnitLength unitLength, Count count, String compoundPattern)1034 public String handleCompoundUnit1(UnitLength unitLength, Count count, String compoundPattern) { 1035 1036 // we want to get a number that works for the count passed in. 1037 @SuppressWarnings("deprecation") 1038 DecimalQuantity amount = getBest(count); 1039 if (amount == null) { 1040 return "n/a"; 1041 } 1042 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(1); 1043 1044 @SuppressWarnings("deprecation") 1045 String form1 = this.pluralInfo.getPluralRules().select(amount); 1046 1047 String pathFormat = "//ldml/units/unitLength" + unitLength.typeString 1048 + "/unit[@type=\"{0}\"]/unitPattern[@count=\"{1}\"]"; 1049 1050 // now pick up the meter pattern 1051 1052 String meterFormat = getValueFromFormat(pathFormat, "length-meter", form1); 1053 1054 // now combine them 1055 1056 String modFormat = combinePrefix(meterFormat, compoundPattern, unitLength == UnitLength.LONG); 1057 1058 return removeEmptyRuns(format(modFormat, numberFormat.format(amount.toBigDecimal()))); 1059 } 1060 1061 // TODO, pass in unitLength instead of last parameter, and do work in Units.combinePattern. 1062 combinePrefix(String unitFormat, String inCompoundPattern, boolean lowercaseUnitIfNoSpaceInCompound)1063 public String combinePrefix(String unitFormat, String inCompoundPattern, boolean lowercaseUnitIfNoSpaceInCompound) { 1064 // mark the part except for the {0} as foreground 1065 String compoundPattern = backgroundEndSymbol 1066 + inCompoundPattern.replace("{0}", backgroundStartSymbol + "{0}" + backgroundEndSymbol) 1067 + backgroundStartSymbol; 1068 1069 String modFormat = Units.combinePattern(unitFormat, compoundPattern, lowercaseUnitIfNoSpaceInCompound); 1070 1071 return backgroundStartSymbol + modFormat + backgroundEndSymbol; 1072 } 1073 1074 //ldml/units/unitLength[@type="long"]/compoundUnit[@type="per"]/compoundUnitPattern makeCompoundUnitPath(UnitLength unitLength, String compoundType, String patternType)1075 public String makeCompoundUnitPath(UnitLength unitLength, String compoundType, String patternType) { 1076 return "//ldml/units/unitLength" + unitLength.typeString 1077 + "/compoundUnit[@type=\"" + compoundType + "\"]" 1078 + "/" + patternType; 1079 } 1080 1081 @SuppressWarnings("deprecation") getBest(Count count)1082 private DecimalQuantity getBest(Count count) { 1083 DecimalQuantitySamples samples = pluralInfo.getPluralRules().getDecimalSamples(count.name(), SampleType.DECIMAL); 1084 if (samples == null) { 1085 samples = pluralInfo.getPluralRules().getDecimalSamples(count.name(), SampleType.INTEGER); 1086 } 1087 if (samples == null) { 1088 return null; 1089 } 1090 Set<DecimalQuantitySamplesRange> samples2 = samples.getSamples(); 1091 DecimalQuantitySamplesRange range = samples2.iterator().next(); 1092 return range.end; 1093 } 1094 handleMiscPatterns(XPathParts parts, String value)1095 private String handleMiscPatterns(XPathParts parts, String value) { 1096 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(0); 1097 String start = backgroundStartSymbol + numberFormat.format(99) + backgroundEndSymbol; 1098 if ("range".equals(parts.getAttributeValue(-1, "type"))) { 1099 String end = backgroundStartSymbol + numberFormat.format(144) + backgroundEndSymbol; 1100 return format(value, start, end); 1101 } else { 1102 return format(value, start); 1103 } 1104 } 1105 handleIntervalFormats(XPathParts parts, String value)1106 private String handleIntervalFormats(XPathParts parts, String value) { 1107 if (!parts.getAttributeValue(3, "type").equals("gregorian")) { 1108 return null; 1109 } 1110 if (parts.getElement(6).equals("intervalFormatFallback")) { 1111 SimpleDateFormat dateFormat = new SimpleDateFormat(); 1112 String fallbackFormat = invertBackground(setBackground(value)); 1113 return format(fallbackFormat, dateFormat.format(FIRST_INTERVAL), 1114 dateFormat.format(SECOND_INTERVAL.get("y"))); 1115 } 1116 String greatestDifference = parts.getAttributeValue(-1, "id"); 1117 /* 1118 * Choose an example interval suitable for the symbol. If testing years, use an interval 1119 * of more than one year, and so forth. For the purpose of choosing the interval, 1120 * "H" is equivalent to "h", and so forth; map to a symbol that occurs in SECOND_INTERVAL. 1121 */ 1122 if (greatestDifference.equals("H")) { // Hour [0-23] 1123 greatestDifference = "h"; // Hour [1-12] 1124 } else if (greatestDifference.equals("B") // flexible day periods 1125 || greatestDifference.equals("b")) { // am, pm, noon, midnight 1126 greatestDifference = "a"; // AM, PM 1127 } 1128 // intervalFormatFallback 1129 // //ldml/dates/calendars/calendar[@type="gregorian"]/dateTimeFormats/intervalFormats/intervalFormatItem[@id="yMd"]/greatestDifference[@id="y"] 1130 // find where to split the value 1131 intervalFormat.setPattern(parts, value); 1132 Date later = SECOND_INTERVAL.get(greatestDifference); 1133 if (later == null) { 1134 /* 1135 * This may still happen for some less-frequently used symbols such as "Q" (Quarter), 1136 * if they ever occur in the data. 1137 * Reference: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 1138 * For now, such paths do not get examples. 1139 */ 1140 return null; 1141 } 1142 return intervalFormat.format(FIRST_INTERVAL, later); 1143 } 1144 handleDelimiters(XPathParts parts, String xpath, String value)1145 private String handleDelimiters(XPathParts parts, String xpath, String value) { 1146 String lastElement = parts.getElement(-1); 1147 final String[] elements = { 1148 "quotationStart", "alternateQuotationStart", 1149 "alternateQuotationEnd", "quotationEnd" }; 1150 String[] quotes = new String[4]; 1151 String baseXpath = xpath.substring(0, xpath.lastIndexOf('/')); 1152 for (int i = 0; i < quotes.length; i++) { 1153 String currElement = elements[i]; 1154 if (lastElement.equals(currElement)) { 1155 quotes[i] = backgroundStartSymbol + value + backgroundEndSymbol; 1156 } else { 1157 quotes[i] = cldrFile.getWinningValue(baseXpath + '/' + currElement); 1158 } 1159 } 1160 String example = cldrFile 1161 .getStringValue("//ldml/localeDisplayNames/types/type[@key=\"calendar\"][@type=\"gregorian\"]"); 1162 // NOTE: the example provided here is partially in English because we don't 1163 // have a translated conversational example in CLDR. 1164 return invertBackground(format("{0}They said {1}" + example + "{2}.{3}", (Object[]) quotes)); 1165 } 1166 handleListPatterns(XPathParts parts, String value)1167 private String handleListPatterns(XPathParts parts, String value) { 1168 // listPatternType is either "duration" or null/other list 1169 String listPatternType = parts.getAttributeValue(-2, "type"); 1170 if (listPatternType == null || !listPatternType.contains("unit")) { 1171 return handleRegularListPatterns(parts, value, ListTypeLength.from(listPatternType)); 1172 } else { 1173 return handleDurationListPatterns(parts, value, UnitLength.from(listPatternType)); 1174 } 1175 } 1176 handleRegularListPatterns(XPathParts parts, String value, ListTypeLength listTypeLength)1177 private String handleRegularListPatterns(XPathParts parts, String value, ListTypeLength listTypeLength) { 1178 String patternType = parts.getAttributeValue(-1, "type"); 1179 if (patternType == null) { 1180 return null; // formerly happened for some "/alias" paths 1181 } 1182 String pathFormat = "//ldml/localeDisplayNames/territories/territory[@type=\"{0}\"]"; 1183 String territory1 = getValueFromFormat(pathFormat, "CH"); 1184 String territory2 = getValueFromFormat(pathFormat, "JP"); 1185 if (patternType.equals("2")) { 1186 return invertBackground(format(setBackground(value), territory1, territory2)); 1187 } 1188 String territory3 = getValueFromFormat(pathFormat, "EG"); 1189 String territory4 = getValueFromFormat(pathFormat, "CA"); 1190 return longListPatternExample( 1191 listTypeLength.getPath(), patternType, value, territory1, territory2, territory3, territory4); 1192 } 1193 handleDurationListPatterns(XPathParts parts, String value, UnitLength unitWidth)1194 private String handleDurationListPatterns(XPathParts parts, String value, UnitLength unitWidth) { 1195 String patternType = parts.getAttributeValue(-1, "type"); 1196 if (patternType == null) { 1197 return null; // formerly happened for some "/alias" paths 1198 } 1199 String duration1 = getFormattedUnit("duration-day", unitWidth, 4); 1200 String duration2 = getFormattedUnit("duration-hour", unitWidth, 2); 1201 if (patternType.equals("2")) { 1202 return invertBackground(format(setBackground(value), duration1, duration2)); 1203 } 1204 String duration3 = getFormattedUnit("duration-minute", unitWidth, 37); 1205 String duration4 = getFormattedUnit("duration-second", unitWidth, 23); 1206 return longListPatternExample( 1207 unitWidth.listTypeLength.getPath(), patternType, value, duration1, duration2, duration3, duration4); 1208 } 1209 1210 public enum UnitLength { 1211 LONG(ListTypeLength.UNIT_WIDE), SHORT(ListTypeLength.UNIT_SHORT), NARROW(ListTypeLength.UNIT_NARROW); 1212 final String typeString; 1213 final ListTypeLength listTypeLength; 1214 UnitLength(ListTypeLength listTypeLength)1215 UnitLength(ListTypeLength listTypeLength) { 1216 typeString = "[@type=\"" + name().toLowerCase(Locale.ENGLISH) + "\"]"; 1217 this.listTypeLength = listTypeLength; 1218 } 1219 from(String listPatternType)1220 public static UnitLength from(String listPatternType) { 1221 if (listPatternType.equals("unit")) { 1222 return UnitLength.LONG; 1223 } else if (listPatternType.equals("unit-narrow")) { 1224 return UnitLength.NARROW; 1225 } else if (listPatternType.equals("unit-short")) { 1226 return UnitLength.SHORT; 1227 } else { 1228 throw new IllegalArgumentException(); 1229 } 1230 } 1231 } 1232 1233 @SuppressWarnings("deprecation") getFormattedUnit(String unitType, UnitLength unitWidth, DecimalQuantity unitAmount)1234 private String getFormattedUnit(String unitType, UnitLength unitWidth, DecimalQuantity unitAmount) { 1235 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(1); 1236 return getFormattedUnit(unitType, unitWidth, unitAmount, numberFormat.format(unitAmount.toBigDecimal())); 1237 } 1238 1239 @SuppressWarnings("deprecation") getFormattedUnit(String unitType, UnitLength unitWidth, double unitAmount)1240 private String getFormattedUnit(String unitType, UnitLength unitWidth, double unitAmount) { 1241 return getFormattedUnit(unitType, unitWidth, new DecimalQuantity_DualStorageBCD(unitAmount)); 1242 } 1243 1244 @SuppressWarnings("deprecation") getFormattedUnit(String unitType, UnitLength unitWidth, DecimalQuantity unitAmount, String formattedUnitAmount)1245 private String getFormattedUnit(String unitType, UnitLength unitWidth, DecimalQuantity unitAmount, String formattedUnitAmount) { 1246 String form = this.pluralInfo.getPluralRules().select(unitAmount); 1247 String pathFormat = "//ldml/units/unitLength" + unitWidth.typeString 1248 + "/unit[@type=\"{0}\"]/unitPattern[@count=\"{1}\"]"; 1249 return format(getValueFromFormat(pathFormat, unitType, form), formattedUnitAmount); 1250 } 1251 1252 //ldml/listPatterns/listPattern/listPatternPart[@type="2"] — And 1253 //ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"] Short And 1254 //ldml/listPatterns/listPattern[@type="or"]/listPatternPart[@type="2"] or list 1255 //ldml/listPatterns/listPattern[@type="unit"]/listPatternPart[@type="2"] 1256 //ldml/listPatterns/listPattern[@type="unit-short"]/listPatternPart[@type="2"] 1257 //ldml/listPatterns/listPattern[@type="unit-narrow"]/listPatternPart[@type="2"] 1258 longListPatternExample(String listPathFormat, String patternType, String value, String... items)1259 private String longListPatternExample(String listPathFormat, String patternType, String value, String... items) { 1260 String doublePattern = getPattern(listPathFormat, "2", patternType, value); 1261 String startPattern = getPattern(listPathFormat, "start", patternType, value); 1262 String middlePattern = getPattern(listPathFormat, "middle", patternType, value); 1263 String endPattern = getPattern(listPathFormat, "end", patternType, value); 1264 /* 1265 * DateTimePatternGenerator.FormatParser is deprecated, but deprecated in ICU is 1266 * also used to mark internal methods (which are OK for us to use in CLDR). 1267 */ 1268 @SuppressWarnings("deprecation") 1269 ListFormatter listFormatter = new ListFormatter(doublePattern, startPattern, middlePattern, endPattern); 1270 String example = listFormatter.format((Object[]) items); 1271 return invertBackground(example); 1272 } 1273 1274 /** 1275 * Helper method for handleListPatterns. Returns the pattern to be used for 1276 * a specified pattern type. 1277 * 1278 * @param pathFormat 1279 * @param pathPatternType 1280 * @param valuePatternType 1281 * @param value 1282 * @return 1283 */ getPattern(String pathFormat, String pathPatternType, String valuePatternType, String value)1284 private String getPattern(String pathFormat, String pathPatternType, String valuePatternType, String value) { 1285 return valuePatternType.equals(pathPatternType) ? setBackground(value) : getValueFromFormat(pathFormat, pathPatternType); 1286 } 1287 getValueFromFormat(String format, Object... arguments)1288 private String getValueFromFormat(String format, Object... arguments) { 1289 return cldrFile.getWinningValue(format(format, arguments)); 1290 } 1291 handleEllipsis(String type, String value)1292 public String handleEllipsis(String type, String value) { 1293 String pathFormat = "//ldml/localeDisplayNames/territories/territory[@type=\"{0}\"]"; 1294 // <ellipsis type="word-final">{0} …</ellipsis> 1295 // <ellipsis type="word-initial">… {0}</ellipsis> 1296 // <ellipsis type="word-medial">{0} … {1}</ellipsis> 1297 String territory1 = getValueFromFormat(pathFormat, "CH"); 1298 String territory2 = getValueFromFormat(pathFormat, "JP"); 1299 // if it isn't a word, break in the middle 1300 if (!type.contains("word")) { 1301 territory1 = clip(territory1, 0, 1); 1302 territory2 = clip(territory2, 1, 0); 1303 } 1304 if (type.contains("initial")) { 1305 territory1 = territory2; 1306 } 1307 return invertBackground(format(setBackground(value), territory1, territory2)); 1308 } 1309 clip(String text, int clipStart, int clipEnd)1310 public static String clip(String text, int clipStart, int clipEnd) { 1311 BreakIterator bi = BreakIterator.getCharacterInstance(); 1312 bi.setText(text); 1313 for (int i = 0; i < clipStart; ++i) { 1314 bi.next(); 1315 } 1316 int start = bi.current(); 1317 bi.last(); 1318 for (int i = 0; i < clipEnd; ++i) { 1319 bi.previous(); 1320 } 1321 int end = bi.current(); 1322 return start >= end ? text : text.substring(start, end); 1323 } 1324 1325 /** 1326 * Handle miscellaneous calendar patterns. 1327 * 1328 * @param parts 1329 * @param value 1330 * @return 1331 */ handleMonthPatterns(XPathParts parts, String value)1332 private String handleMonthPatterns(XPathParts parts, String value) { 1333 String calendar = parts.getAttributeValue(3, "type"); 1334 String context = parts.getAttributeValue(5, "type"); 1335 String month = "8"; 1336 if (!context.equals("numeric")) { 1337 String width = parts.getAttributeValue(6, "type"); 1338 String xpath = "//ldml/dates/calendars/calendar[@type=\"{0}\"]/months/monthContext[@type=\"{1}\"]/monthWidth[@type=\"{2}\"]/month[@type=\"8\"]"; 1339 month = getValueFromFormat(xpath, calendar, context, width); 1340 } 1341 return invertBackground(format(setBackground(value), month)); 1342 } 1343 handleAppendItems(XPathParts parts, String value)1344 private String handleAppendItems(XPathParts parts, String value) { 1345 String request = parts.getAttributeValue(-1, "request"); 1346 if (!"Timezone".equals(request)) { 1347 return null; 1348 } 1349 String calendar = parts.getAttributeValue(3, "type"); 1350 1351 SimpleDateFormat sdf = icuServiceBuilder.getDateFormat(calendar, 0, DateFormat.MEDIUM, null); 1352 String zone = cldrFile.getStringValue("//ldml/dates/timeZoneNames/gmtZeroFormat"); 1353 String result = format(value, setBackground(sdf.format(DATE_SAMPLE)), setBackground(zone)); 1354 return result; 1355 } 1356 1357 private class IntervalFormat { 1358 @SuppressWarnings("deprecation") 1359 DateTimePatternGenerator.FormatParser formatParser = new DateTimePatternGenerator.FormatParser(); 1360 SimpleDateFormat firstFormat = new SimpleDateFormat(); 1361 SimpleDateFormat secondFormat = new SimpleDateFormat(); 1362 StringBuilder first = new StringBuilder(); 1363 StringBuilder second = new StringBuilder(); 1364 BitSet letters = new BitSet(); 1365 format(Date earlier, Date later)1366 public String format(Date earlier, Date later) { 1367 if (earlier == null || later == null) { 1368 return null; 1369 } 1370 if (later.compareTo(earlier) < 0) { 1371 /* 1372 * Swap so earlier is earlier than later. 1373 * This is necessary for "G" (Era) given the current FIRST_INTERVAL, SECOND_INTERVAL 1374 */ 1375 Date tmp = earlier; 1376 earlier = later; 1377 later = tmp; 1378 } 1379 return firstFormat.format(earlier) + secondFormat.format(later); 1380 } 1381 1382 @SuppressWarnings("deprecation") setPattern(XPathParts parts, String pattern)1383 public IntervalFormat setPattern(XPathParts parts, String pattern) { 1384 if (formatParser == null || pattern == null) { 1385 return this; 1386 } 1387 try { 1388 formatParser.set(pattern); 1389 } catch (NullPointerException e) { 1390 /* 1391 * This has been observed to occur, within ICU, for unknown reasons. 1392 */ 1393 System.err.println("Caught NullPointerException in IntervalFormat.setPattern, pattern = " + pattern); 1394 e.printStackTrace(); 1395 return null; 1396 } 1397 first.setLength(0); 1398 second.setLength(0); 1399 boolean doFirst = true; 1400 letters.clear(); 1401 1402 for (Object item : formatParser.getItems()) { 1403 if (item instanceof DateTimePatternGenerator.VariableField) { 1404 char c = item.toString().charAt(0); 1405 if (letters.get(c)) { 1406 doFirst = false; 1407 } else { 1408 letters.set(c); 1409 } 1410 if (doFirst) { 1411 first.append(item); 1412 } else { 1413 second.append(item); 1414 } 1415 } else { 1416 if (doFirst) { 1417 first.append(formatParser.quoteLiteral((String) item)); 1418 } else { 1419 second.append(formatParser.quoteLiteral((String) item)); 1420 } 1421 } 1422 } 1423 String calendar = parts.findAttributeValue("calendar", "type"); 1424 firstFormat = icuServiceBuilder.getDateFormat(calendar, first.toString()); 1425 firstFormat.setTimeZone(GMT_ZONE_SAMPLE); 1426 1427 secondFormat = icuServiceBuilder.getDateFormat(calendar, second.toString()); 1428 secondFormat.setTimeZone(GMT_ZONE_SAMPLE); 1429 return this; 1430 } 1431 } 1432 handleDurationUnit(String value)1433 private String handleDurationUnit(String value) { 1434 DateFormat df = this.icuServiceBuilder.getDateFormat("gregorian", value.replace('h', 'H')); 1435 df.setTimeZone(TimeZone.GMT_ZONE); 1436 long time = ((5 * 60 + 37) * 60 + 23) * 1000; 1437 try { 1438 return df.format(new Date(time)); 1439 } catch (IllegalArgumentException e) { 1440 // e.g., Illegal pattern character 'o' in "aɖabaƒoƒo m:ss" 1441 return null; 1442 } 1443 } 1444 1445 @SuppressWarnings("deprecation") formatCountValue(String xpath, XPathParts parts, String value)1446 private String formatCountValue(String xpath, XPathParts parts, String value) { 1447 if (!parts.containsAttribute("count")) { // no examples for items that don't format 1448 return null; 1449 } 1450 final PluralInfo plurals = supplementalDataInfo.getPlurals(PluralType.cardinal, cldrFile.getLocaleID()); 1451 PluralRules pluralRules = plurals.getPluralRules(); 1452 1453 String unitType = parts.getAttributeValue(-2, "type"); 1454 if (unitType == null) { 1455 unitType = "USD"; // sample for currency pattern 1456 } 1457 final boolean isPattern = parts.contains("unitPattern"); 1458 final boolean isCurrency = !parts.contains("units"); 1459 1460 Count count = null; 1461 final LinkedHashSet<DecimalQuantity> exampleCount = new LinkedHashSet<>(); 1462 exampleCount.addAll(CURRENCY_SAMPLES); 1463 String countString = parts.getAttributeValue(-1, "count"); 1464 if (countString == null) { 1465 // count = Count.one; 1466 return null; 1467 } else { 1468 try { 1469 count = Count.valueOf(countString); 1470 } catch (Exception e) { 1471 return null; // counts like 0 1472 } 1473 } 1474 1475 // we used to just get the samples for the given keyword, but that doesn't work well any more. 1476 getStartEndSamples(pluralRules.getDecimalSamples(countString, SampleType.INTEGER), exampleCount); 1477 getStartEndSamples(pluralRules.getDecimalSamples(countString, SampleType.DECIMAL), exampleCount); 1478 1479 String result = ""; 1480 DecimalFormat currencyFormat = icuServiceBuilder.getCurrencyFormat(unitType); 1481 int decimalCount = currencyFormat.getMinimumFractionDigits(); 1482 1483 // Unless/until DecimalQuantity overrides hashCode() or implements Comparable, we 1484 // should use a concrete collection type for examplesSeen for which .contains() only 1485 // relies on DecimalQuantity.equals() . The reason is that the default hashCode() 1486 // implementation for DecimalQuantity may return false when .equals() returns true. 1487 Collection<DecimalQuantity> examplesSeen = new ArrayList<>(); 1488 1489 // we will cycle until we have (at most) two examples. 1490 int maxCount = 2; 1491 main: 1492 // If we are a currency, we will try to see if we can set the decimals to match. 1493 // but if nothing works, we will just use a plain sample. 1494 for (int phase = 0; phase < 2; ++phase) { 1495 for (DecimalQuantity example : exampleCount) { 1496 // we have to first see whether we have a currency. If so, we have to see if the count works. 1497 1498 if (isCurrency && phase == 0) { 1499 DecimalQuantity_DualStorageBCD newExample = new DecimalQuantity_DualStorageBCD(); 1500 newExample.copyFrom(example); 1501 newExample.setMinFraction(decimalCount); 1502 example = newExample; 1503 } 1504 // skip if we've done before (can happen because of the currency reset) 1505 if (examplesSeen.contains(example)) { 1506 continue; 1507 } 1508 examplesSeen.add(example); 1509 // skip if the count isn't appropriate 1510 if (!pluralRules.select(example).equals(count.toString())) { 1511 continue; 1512 } 1513 1514 if (value == null) { 1515 String fallbackPath = cldrFile.getCountPathWithFallback(xpath, count, true); 1516 value = cldrFile.getStringValue(fallbackPath); 1517 } 1518 String resultItem; 1519 1520 resultItem = formatCurrency(value, unitType, isPattern, isCurrency, count, example); 1521 // now add to list 1522 result = addExampleResult(resultItem, result); 1523 if (isPattern) { 1524 String territory = getDefaultTerritory(); 1525 String currency = supplementalDataInfo.getDefaultCurrency(territory); 1526 if (currency.equals(unitType)) { 1527 currency = "EUR"; 1528 if (currency.equals(unitType)) { 1529 currency = "JAY"; 1530 } 1531 } 1532 resultItem = formatCurrency(value, currency, isPattern, isCurrency, count, example); 1533 // now add to list 1534 result = addExampleResult(resultItem, result); 1535 1536 } 1537 if (--maxCount < 1) { 1538 break main; 1539 } 1540 } 1541 } 1542 return result.isEmpty() ? null : result; 1543 } 1544 1545 @SuppressWarnings("deprecation") getStartEndSamples(DecimalQuantitySamples samples, Set<DecimalQuantity> target)1546 static public void getStartEndSamples(DecimalQuantitySamples samples, Set<DecimalQuantity> target) { 1547 if (samples != null) { 1548 for (DecimalQuantitySamplesRange item : samples.getSamples()) { 1549 target.add(item.start); 1550 target.add(item.end); 1551 } 1552 } 1553 } 1554 1555 @SuppressWarnings("deprecation") formatCurrency(String value, String unitType, final boolean isPattern, final boolean isCurrency, Count count, DecimalQuantity example)1556 private String formatCurrency(String value, String unitType, final boolean isPattern, final boolean isCurrency, Count count, 1557 DecimalQuantity example) { 1558 String resultItem; 1559 { 1560 // If we have a pattern, get the unit from the count 1561 // If we have a unit, get the pattern from the count 1562 // English is special; both values are retrieved based on the count. 1563 String unitPattern; 1564 String unitName; 1565 if (isPattern) { 1566 // //ldml/numbers/currencies/currency[@type="USD"]/displayName 1567 unitName = getUnitName(unitType, isCurrency, count); 1568 unitPattern = typeIsEnglish ? getUnitPattern(unitType, isCurrency, count) : value; 1569 } else { 1570 unitPattern = getUnitPattern(unitType, isCurrency, count); 1571 unitName = typeIsEnglish ? getUnitName(unitType, isCurrency, count) : value; 1572 } 1573 1574 if (isPattern) { 1575 unitPattern = setBackground(unitPattern); 1576 } else { 1577 unitPattern = setBackgroundExceptMatch(unitPattern, PARAMETER_SKIP0); 1578 } 1579 1580 MessageFormat unitPatternFormat = new MessageFormat(unitPattern); 1581 1582 // get the format for the currency 1583 // TODO fix this for special currency overrides 1584 1585 DecimalFormat unitDecimalFormat = icuServiceBuilder.getNumberFormat(1); // decimal 1586 unitDecimalFormat.setMaximumFractionDigits((int) example.getPluralOperand(Operand.v)); 1587 unitDecimalFormat.setMinimumFractionDigits((int) example.getPluralOperand(Operand.v)); 1588 1589 String formattedNumber = unitDecimalFormat.format(example.toDouble()); 1590 unitPatternFormat.setFormatByArgumentIndex(0, unitDecimalFormat); 1591 resultItem = unitPattern.replace("{0}", formattedNumber).replace("{1}", unitName); 1592 1593 if (isPattern) { 1594 resultItem = invertBackground(resultItem); 1595 } 1596 } 1597 return resultItem; 1598 } 1599 addExampleResult(String resultItem, String resultToAddTo)1600 private String addExampleResult(String resultItem, String resultToAddTo) { 1601 return addExampleResult(resultItem, resultToAddTo, false); 1602 } 1603 addExampleResult(String resultItem, String resultToAddTo, boolean showContexts)1604 private String addExampleResult(String resultItem, String resultToAddTo, boolean showContexts) { 1605 if (!showContexts) { 1606 if (resultToAddTo.length() != 0) { 1607 resultToAddTo += exampleSeparatorSymbol; 1608 } 1609 resultToAddTo += resultItem; 1610 } else { 1611 resultToAddTo += exampleStartAutoSymbol + resultItem + exampleEndSymbol; // example in neutral context 1612 resultToAddTo += exampleStartRTLSymbol + resultItem + exampleEndSymbol; // example in RTL context 1613 } 1614 return resultToAddTo; 1615 } 1616 getUnitPattern(String unitType, final boolean isCurrency, Count count)1617 private String getUnitPattern(String unitType, final boolean isCurrency, Count count) { 1618 return cldrFile.getStringValue(isCurrency 1619 ? "//ldml/numbers/currencyFormats/unitPattern" + countAttribute(count) 1620 : "//ldml/units/unit[@type=\"" + unitType + "\"]/unitPattern" + countAttribute(count)); 1621 } 1622 getUnitName(String unitType, final boolean isCurrency, Count count)1623 private String getUnitName(String unitType, final boolean isCurrency, Count count) { 1624 return cldrFile.getStringValue(isCurrency 1625 ? "//ldml/numbers/currencies/currency[@type=\"" + unitType + "\"]/displayName" + countAttribute(count) 1626 : "//ldml/units/unit[@type=\"" + unitType + "\"]/unitPattern" + countAttribute(count)); 1627 } 1628 countAttribute(Count count)1629 public String countAttribute(Count count) { 1630 return "[@count=\"" + count + "\"]"; 1631 } 1632 handleNumberSymbol(XPathParts parts, String value)1633 private String handleNumberSymbol(XPathParts parts, String value) { 1634 String symbolType = parts.getElement(-1); 1635 String numberSystem = parts.getAttributeValue(2, "numberSystem"); // null if not present 1636 int index = 1;// dec/percent/sci 1637 double numberSample = NUMBER_SAMPLE; 1638 String originalValue = cldrFile.getWinningValue(parts.toString()); 1639 boolean isSuperscripting = false; 1640 if (symbolType.equals("decimal") || symbolType.equals("group")) { 1641 index = 1; 1642 } else if (symbolType.equals("minusSign")) { 1643 index = 1; 1644 numberSample = -numberSample; 1645 } else if (symbolType.equals("percentSign")) { 1646 // For the perMille symbol, we reuse the percent example. 1647 index = 2; 1648 numberSample = 0.23; 1649 } else if (symbolType.equals("perMille")) { 1650 // For the perMille symbol, we reuse the percent example. 1651 index = 2; 1652 numberSample = 0.023; 1653 originalValue = cldrFile.getWinningValue(parts.addRelative("../percentSign").toString()); 1654 } else if (symbolType.equals("approximatelySign")) { 1655 // Substitute the approximately symbol in for the minus sign. 1656 index = 1; 1657 numberSample = -numberSample; 1658 originalValue = cldrFile.getWinningValue(parts.addRelative("../minusSign").toString()); 1659 } else if (symbolType.equals("exponential") || symbolType.equals("plusSign")) { 1660 index = 3; 1661 } else if (symbolType.equals("superscriptingExponent")) { 1662 index = 3; 1663 isSuperscripting = true; 1664 } else { 1665 // We don't need examples for standalone symbols, i.e. infinity and nan. 1666 // We don't have an example for the list symbol either. 1667 return null; 1668 } 1669 DecimalFormat x = icuServiceBuilder.getNumberFormat(index, numberSystem); 1670 String example; 1671 String formattedValue; 1672 if (isSuperscripting) { 1673 DecimalFormatSymbols symbols = x.getDecimalFormatSymbols(); 1674 char[] digits = symbols.getDigits(); 1675 x.setDecimalFormatSymbols(symbols); 1676 x.setNegativeSuffix(endSupSymbol + x.getNegativeSuffix()); 1677 x.setPositiveSuffix(endSupSymbol + x.getPositiveSuffix()); 1678 x.setExponentSignAlwaysShown(false); 1679 1680 // Don't set the exponent directly because future examples for items 1681 // will be affected as well. 1682 originalValue = symbols.getExponentSeparator(); 1683 formattedValue = backgroundEndSymbol + value + digits[1] + digits[0] + backgroundStartSymbol + startSupSymbol; 1684 example = x.format(numberSample); 1685 } else { 1686 x.setExponentSignAlwaysShown(true); 1687 formattedValue = backgroundEndSymbol + value + backgroundStartSymbol; 1688 } 1689 example = x.format(numberSample); 1690 example = example.replace(originalValue, formattedValue); 1691 return backgroundStartSymbol + example + backgroundEndSymbol; 1692 } 1693 handleNumberingSystem(String value)1694 private String handleNumberingSystem(String value) { 1695 NumberFormat x = icuServiceBuilder.getGenericNumberFormat(value); 1696 x.setGroupingUsed(false); 1697 return x.format(NUMBER_SAMPLE_WHOLE); 1698 } 1699 handleTimeZoneName(XPathParts parts, String value)1700 private String handleTimeZoneName(XPathParts parts, String value) { 1701 String result = null; 1702 if (parts.contains("exemplarCity")) { 1703 // ldml/dates/timeZoneNames/zone[@type="America/Los_Angeles"]/exemplarCity 1704 String timezone = parts.getAttributeValue(3, "type"); 1705 String countryCode = supplementalDataInfo.getZone_territory(timezone); 1706 if (countryCode == null) { 1707 if (value == null) { 1708 result = timezone.substring(timezone.lastIndexOf('/') + 1).replace('_', ' '); 1709 } else { 1710 result = value; 1711 } 1712 return result; 1713 } 1714 if (countryCode.equals("001")) { 1715 // GMT code, so format. 1716 try { 1717 String hourOffset = timezone.substring(timezone.contains("+") ? 8 : 7); 1718 int hours = Integer.parseInt(hourOffset); 1719 result = getGMTFormat(null, null, hours); 1720 } catch (RuntimeException e) { 1721 return result; // fail, skip 1722 } 1723 } else { 1724 result = setBackground(cldrFile.getName(CLDRFile.TERRITORY_NAME, countryCode)); 1725 } 1726 } else if (parts.contains("zone")) { // {0} Time 1727 result = value; 1728 } else if (parts.contains("regionFormat")) { // {0} Time 1729 result = format(value, setBackground(cldrFile.getName(CLDRFile.TERRITORY_NAME, "JP"))); 1730 result = addExampleResult( 1731 format(value, setBackground(cldrFile.getWinningValue(EXEMPLAR_CITY_LOS_ANGELES))), result); 1732 } else if (parts.contains("fallbackFormat")) { // {1} ({0}) 1733 String central = setBackground(cldrFile.getWinningValue("//ldml/dates/timeZoneNames/metazone[@type=\"America_Central\"]/long/generic")); 1734 String cancun = setBackground(cldrFile.getWinningValue("//ldml/dates/timeZoneNames/zone[@type=\"America/Cancun\"]/exemplarCity")); 1735 result = format(value, cancun, central); 1736 } else if (parts.contains("gmtFormat")) { // GMT{0} 1737 result = getGMTFormat(null, value, -8); 1738 } else if (parts.contains("hourFormat")) { // +HH:mm;-HH:mm 1739 result = getGMTFormat(value, null, -8); 1740 } else if (parts.contains("metazone") && !parts.contains("commonlyUsed")) { // Metazone string 1741 if (value != null && value.length() > 0) { 1742 result = getMZTimeFormat() + " " + value; 1743 } else { 1744 // TODO check for value 1745 if (parts.contains("generic")) { 1746 String metazone_name = parts.getAttributeValue(3, "type"); 1747 String timezone = supplementalDataInfo.getZoneForMetazoneByRegion(metazone_name, "001"); 1748 String countryCode = supplementalDataInfo.getZone_territory(timezone); 1749 String regionFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/regionFormat"); 1750 String exemplarCity = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/zone[@type=\"" 1751 + timezone + "\"]/exemplarCity"); 1752 if (exemplarCity == null) { 1753 exemplarCity = timezone.substring(timezone.lastIndexOf('/') + 1).replace('_', ' '); 1754 } 1755 String countryName = cldrFile 1756 .getWinningValue("//ldml/localeDisplayNames/territories/territory[@type=\"" + countryCode 1757 + "\"]"); 1758 result = setBackground(getMZTimeFormat() + " " + 1759 format(regionFormat, countryName)); 1760 } else { 1761 String gmtFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/gmtFormat"); 1762 String hourFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/hourFormat"); 1763 String metazone_name = parts.getAttributeValue(3, "type"); 1764 String tz_string = supplementalDataInfo.getZoneForMetazoneByRegion(metazone_name, "001"); 1765 TimeZone currentZone = TimeZone.getTimeZone(tz_string); 1766 int tzOffset = currentZone.getRawOffset(); 1767 if (parts.contains("daylight")) { 1768 tzOffset += currentZone.getDSTSavings(); 1769 } 1770 int MILLIS_PER_MINUTE = 1000 * 60; 1771 int MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60; 1772 int tm_hrs = tzOffset / MILLIS_PER_HOUR; 1773 int tm_mins = (tzOffset % MILLIS_PER_HOUR) / 60000; // millis per minute 1774 result = setBackground(getMZTimeFormat() + " " 1775 + getGMTFormat(hourFormat, gmtFormat, tm_hrs, tm_mins)); 1776 } 1777 } 1778 } 1779 return result; 1780 } 1781 1782 @SuppressWarnings("deprecation") handleDateFormatItem(String xpath, String value, boolean showContexts)1783 private String handleDateFormatItem(String xpath, String value, boolean showContexts) { 1784 // Get here if parts contains "calendar" and either of "pattern", "dateFormatItem" 1785 1786 String fullpath = cldrFile.getFullXPath(xpath); 1787 XPathParts parts = XPathParts.getFrozenInstance(fullpath); 1788 String calendar = parts.findAttributeValue("calendar", "type"); 1789 1790 if (parts.contains("dateTimeFormat")) { // date-time combining patterns 1791 String dateFormatXPath = cldrFile.getWinningPath(xpath.replaceAll("dateTimeFormat", "dateFormat").replaceAll("atTime", "standard")); 1792 String timeFormatXPath = cldrFile.getWinningPath(xpath.replaceAll("dateTimeFormat", "timeFormat").replaceAll("atTime", "standard")); 1793 String dateFormatValue = cldrFile.getWinningValue(dateFormatXPath); 1794 String timeFormatValue = cldrFile.getWinningValue(timeFormatXPath); 1795 parts = XPathParts.getFrozenInstance(cldrFile.getFullXPath(dateFormatXPath)); 1796 String dateNumbersOverride = parts.findAttributeValue("pattern", "numbers"); 1797 parts = XPathParts.getFrozenInstance(cldrFile.getFullXPath(timeFormatXPath)); 1798 String timeNumbersOverride = parts.findAttributeValue("pattern", "numbers"); 1799 SimpleDateFormat df = icuServiceBuilder.getDateFormat(calendar, dateFormatValue, dateNumbersOverride); 1800 SimpleDateFormat tf = icuServiceBuilder.getDateFormat(calendar, timeFormatValue, timeNumbersOverride); 1801 df.setTimeZone(ZONE_SAMPLE); 1802 tf.setTimeZone(ZONE_SAMPLE); 1803 String dfResult = "'" + df.format(DATE_SAMPLE) + "'"; 1804 String tfResult = "'" + tf.format(DATE_SAMPLE) + "'"; 1805 SimpleDateFormat dtf = icuServiceBuilder.getDateFormat(calendar, 1806 MessageFormat.format(value, (Object[]) new String[] { setBackground(tfResult), setBackground(dfResult) })); 1807 return dtf.format(DATE_SAMPLE); 1808 } else { 1809 String id = parts.findAttributeValue("dateFormatItem", "id"); 1810 if ("NEW".equals(id) || value == null) { 1811 return startItalicSymbol + "n/a" + endItalicSymbol; 1812 } else { 1813 String numbersOverride = parts.findAttributeValue("pattern", "numbers"); 1814 SimpleDateFormat sdf = icuServiceBuilder.getDateFormat(calendar, value, numbersOverride); 1815 sdf.setTimeZone(ZONE_SAMPLE); 1816 String defaultNumberingSystem = cldrFile.getWinningValue("//ldml/numbers/defaultNumberingSystem"); 1817 String timeSeparator = cldrFile.getWinningValue("//ldml/numbers/symbols[@numberSystem='" + defaultNumberingSystem + "']/timeSeparator"); 1818 DateFormatSymbols dfs = sdf.getDateFormatSymbols(); 1819 dfs.setTimeSeparatorString(timeSeparator); 1820 sdf.setDateFormatSymbols(dfs); 1821 if (id == null || id.indexOf('B') < 0) { 1822 // Standard date/time format, or availableFormat without dayPeriod 1823 if (value.indexOf("MMM") >= 0 || value.indexOf("LLL") >= 0) { 1824 // alpha month, do not need context examples 1825 return sdf.format(DATE_SAMPLE); 1826 } else { 1827 // Use contextExamples if showContexts T 1828 String example = showContexts? exampleStartHeaderSymbol + contextheader + exampleEndSymbol : ""; 1829 example = addExampleResult(sdf.format(DATE_SAMPLE), example, showContexts); 1830 return example; 1831 } 1832 } else { 1833 List<String> examples = new ArrayList<>(); 1834 examples.add(sdf.format(DATE_SAMPLE3)); 1835 examples.add(sdf.format(DATE_SAMPLE)); 1836 examples.add(sdf.format(DATE_SAMPLE4)); 1837 return formatExampleList(examples.toArray(new String[examples.size()])); 1838 } 1839 } 1840 } 1841 } 1842 1843 // Simple check whether the currency symbol has letters on one or both sides symbolIsLetters(String currencySymbol, boolean onBothSides)1844 private boolean symbolIsLetters(String currencySymbol, boolean onBothSides) { 1845 int len = currencySymbol.length(); 1846 if (len == 0) { 1847 return false; 1848 } 1849 int limitChar = currencySymbol.codePointAt(0); 1850 if (UCharacter.isLetter(limitChar)) { 1851 if (!onBothSides) { 1852 return true; 1853 } 1854 } else if (onBothSides) { 1855 return false; 1856 } 1857 if (len > 1) { 1858 limitChar = currencySymbol.codePointAt(len - 1); 1859 if (UCharacter.isLetter(limitChar)) { 1860 return true; 1861 } 1862 } 1863 return false; 1864 } 1865 1866 /** 1867 * Creates examples for currency formats. 1868 * 1869 * @param value 1870 * @return 1871 */ handleCurrencyFormat(XPathParts parts, String value, boolean showContexts)1872 private String handleCurrencyFormat(XPathParts parts, String value, boolean showContexts) { 1873 1874 String example = showContexts? exampleStartHeaderSymbol + contextheader + exampleEndSymbol : ""; 1875 String territory = getDefaultTerritory(); 1876 1877 String currency = supplementalDataInfo.getDefaultCurrency(territory); 1878 String checkPath = "//ldml/numbers/currencies/currency[@type=\"" + currency + "\"]/symbol"; 1879 String currencySymbol = cldrFile.getWinningValue(checkPath); 1880 String altValue = parts.getAttributeValue(-1, "alt"); 1881 boolean altAlpha = (altValue != null && altValue.equals("alphaNextToNumber")); 1882 if (altAlpha && !symbolIsLetters(currencySymbol, true)) { 1883 // If this example is for alt="alphaNextToNumber" and the default currency symbol 1884 // does not have letters on both sides, need to use a fully alphabetic one. 1885 currencySymbol = currency; 1886 } 1887 1888 String numberSystem = parts.getAttributeValue(2, "numberSystem"); // null if not present 1889 1890 DecimalFormat df = icuServiceBuilder.getCurrencyFormat(currency, currencySymbol, numberSystem); 1891 df.applyPattern(value); 1892 1893 String countValue = parts.getAttributeValue(-1, "count"); 1894 if (countValue != null) { 1895 return formatCountDecimal(df, countValue); 1896 } 1897 1898 double sampleAmount = 1295.00; 1899 example = addExampleResult(formatNumber(df, sampleAmount), example, showContexts); 1900 example = addExampleResult(formatNumber(df, -sampleAmount), example, showContexts); 1901 1902 if (showContexts && !altAlpha && countValue == null) { 1903 // If this example is not for alt="alphaNextToNumber", then if the currency symbol 1904 // above has letters (strong dir) add another example with non-letter symbol 1905 // (weak or neutral), or vice versa 1906 if (symbolIsLetters(currencySymbol, false)) { 1907 currency = "EUR"; 1908 checkPath = "//ldml/numbers/currencies/currency[@type=\"" + currency + "\"]/symbol"; 1909 currencySymbol = cldrFile.getWinningValue(checkPath); 1910 } else { 1911 currencySymbol = currency; 1912 } 1913 df = icuServiceBuilder.getCurrencyFormat(currency, currencySymbol, numberSystem); 1914 df.applyPattern(value); 1915 example = addExampleResult(formatNumber(df, sampleAmount), example, showContexts); 1916 example = addExampleResult(formatNumber(df, -sampleAmount), example, showContexts); 1917 } 1918 1919 return example; 1920 } 1921 getDefaultTerritory()1922 private String getDefaultTerritory() { 1923 CLDRLocale loc; 1924 String territory = "US"; 1925 if (!typeIsEnglish) { 1926 loc = CLDRLocale.getInstance(cldrFile.getLocaleID()); 1927 territory = loc.getCountry(); 1928 if (territory == null || territory.length() == 0) { 1929 loc = supplementalDataInfo.getDefaultContentFromBase(loc); 1930 if (loc != null) { 1931 territory = loc.getCountry(); 1932 if (territory.equals("001") && loc.getLanguage().equals("ar")) { 1933 territory = "EG"; // Use Egypt as territory for examples in ar locale, since its default content is ar_001. 1934 } 1935 } 1936 } 1937 if (territory == null || territory.length() == 0) { 1938 territory = "US"; 1939 } 1940 } 1941 return territory; 1942 } 1943 1944 /** 1945 * Creates examples for decimal formats. 1946 * 1947 * @param value 1948 * @return 1949 */ handleDecimalFormat(XPathParts parts, String value, boolean showContexts)1950 private String handleDecimalFormat(XPathParts parts, String value, boolean showContexts) { 1951 String example = showContexts? exampleStartHeaderSymbol + contextheader + exampleEndSymbol : ""; 1952 String numberSystem = parts.getAttributeValue(2, "numberSystem"); // null if not present 1953 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(value, numberSystem); 1954 String countValue = parts.getAttributeValue(-1, "count"); 1955 if (countValue != null) { 1956 return formatCountDecimal(numberFormat, countValue); 1957 } 1958 1959 double sampleNum1 = 5.43; 1960 double sampleNum2 = NUMBER_SAMPLE; 1961 if (parts.getElement(4).equals("percentFormat")) { 1962 sampleNum1 = 0.0543; 1963 } 1964 example = addExampleResult(formatNumber(numberFormat, sampleNum1), example, showContexts); 1965 example = addExampleResult(formatNumber(numberFormat, sampleNum2), example, showContexts); 1966 // have positive and negative 1967 example = addExampleResult(formatNumber(numberFormat, -sampleNum2), example, showContexts); 1968 return example; 1969 } 1970 formatCountDecimal(DecimalFormat numberFormat, String countValue)1971 private String formatCountDecimal(DecimalFormat numberFormat, String countValue) { 1972 Count count; 1973 try { 1974 count = Count.valueOf(countValue); 1975 } catch (Exception e) { 1976 String locale = getCldrFile().getLocaleID(); 1977 PluralInfo pluralInfo = supplementalDataInfo.getPlurals(locale); 1978 count = pluralInfo.getCount(DecimalQuantity_DualStorageBCD.fromExponentString(countValue)); 1979 } 1980 Double numberSample = getExampleForPattern(numberFormat, count); 1981 if (numberSample == null) { 1982 // Ideally, we would suppress the value in the survey tool. 1983 // However, until we switch over to the ICU samples, we are not guaranteed 1984 // that "no samples" means "can't occur". So we manufacture something. 1985 int digits = numberFormat.getMinimumIntegerDigits(); 1986 numberSample = (double) Math.round(1.2345678901234 * Math.pow(10, digits - 1)); 1987 } 1988 String temp = String.valueOf(numberSample); 1989 int fractionLength = temp.endsWith(".0") ? 0 : temp.length() - temp.indexOf('.') - 1; 1990 if (fractionLength != numberFormat.getMaximumFractionDigits()) { 1991 numberFormat = (DecimalFormat) numberFormat.clone(); // for safety 1992 numberFormat.setMinimumFractionDigits(fractionLength); 1993 numberFormat.setMaximumFractionDigits(fractionLength); 1994 } 1995 return formatNumber(numberFormat, numberSample); 1996 } 1997 formatNumber(DecimalFormat format, double value)1998 private String formatNumber(DecimalFormat format, double value) { 1999 String example = format.format(value); 2000 return setBackgroundOnMatch(example, ALL_DIGITS); 2001 } 2002 2003 /** 2004 * Calculates a numerical example to use for the specified pattern using 2005 * brute force (there should be a more elegant way to do this). 2006 * 2007 * @param format 2008 * @param count 2009 * @return 2010 */ getExampleForPattern(DecimalFormat format, Count count)2011 private Double getExampleForPattern(DecimalFormat format, Count count) { 2012 if (patternExamples == null) { 2013 patternExamples = PluralSamples.getInstance(cldrFile.getLocaleID()); 2014 } 2015 int numDigits = format.getMinimumIntegerDigits(); 2016 Map<Count, Double> samples = patternExamples.getSamples(numDigits); 2017 if (samples == null) { 2018 return null; 2019 } 2020 return samples.get(count); 2021 } 2022 handleCurrency(String xpath, XPathParts parts, String value)2023 private String handleCurrency(String xpath, XPathParts parts, String value) { 2024 String currency = parts.getAttributeValue(-2, "type"); 2025 String fullPath = cldrFile.getFullXPath(xpath, false); 2026 if (parts.contains("symbol")) { 2027 if (fullPath != null && fullPath.contains("[@choice=\"true\"]")) { 2028 ChoiceFormat cf = new ChoiceFormat(value); 2029 value = cf.format(NUMBER_SAMPLE); 2030 } 2031 String result; 2032 if (value == null) { 2033 throw new NullPointerException( 2034 cldrFile.getSourceLocation(fullPath) + 2035 ": " + cldrFile.getLocaleID()+ ": " + 2036 ": Error: no currency symbol for " + currency); 2037 } 2038 DecimalFormat x = icuServiceBuilder.getCurrencyFormat(currency, value); 2039 result = x.format(NUMBER_SAMPLE); 2040 result = setBackground(result).replace(value, backgroundEndSymbol + value + backgroundStartSymbol); 2041 return result; 2042 } else if (parts.contains("displayName")) { 2043 return formatCountValue(xpath, parts, value); 2044 } 2045 return null; 2046 } 2047 handleDateRangePattern(String value)2048 private String handleDateRangePattern(String value) { 2049 String result; 2050 SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian", 2, 0); 2051 result = format(value, setBackground(dateFormat.format(DATE_SAMPLE)), 2052 setBackground(dateFormat.format(DATE_SAMPLE2))); 2053 return result; 2054 } 2055 2056 /** 2057 * @param elementToOverride the element that is to be overridden 2058 * @param element the overriding element 2059 * @param value the value to override element with 2060 * @return 2061 */ getLocaleDisplayPattern(String elementToOverride, String element, String value)2062 private String getLocaleDisplayPattern(String elementToOverride, String element, String value) { 2063 final String localeDisplayPatternPath = "//ldml/localeDisplayNames/localeDisplayPattern/"; 2064 if (elementToOverride.equals(element)) { 2065 return value; 2066 } else { 2067 return cldrFile.getWinningValue(localeDisplayPatternPath + elementToOverride); 2068 } 2069 } 2070 handleDisplayNames(String xpath, XPathParts parts, String value)2071 private String handleDisplayNames(String xpath, XPathParts parts, String value) { 2072 String result = null; 2073 if (parts.contains("codePatterns")) { 2074 //ldml/localeDisplayNames/codePatterns/codePattern[@type="language"] 2075 //ldml/localeDisplayNames/codePatterns/codePattern[@type="script"] 2076 //ldml/localeDisplayNames/codePatterns/codePattern[@type="territory"] 2077 String type = parts.getAttributeValue(-1, "type"); 2078 result = format(value, setBackground( 2079 type.equals("language") ? "ace" 2080 : type.equals("script") ? "Avst" 2081 : type.equals("territory") ? "057" : "CODE")); 2082 } else if (parts.contains("localeDisplayPattern")) { 2083 //ldml/localeDisplayNames/localeDisplayPattern/localePattern 2084 //ldml/localeDisplayNames/localeDisplayPattern/localeSeparator 2085 //ldml/localeDisplayNames/localeDisplayPattern/localeKeyTypePattern 2086 String element = parts.getElement(-1); 2087 value = setBackground(value); 2088 String localeKeyTypePattern = getLocaleDisplayPattern("localeKeyTypePattern", element, value); 2089 String localePattern = getLocaleDisplayPattern("localePattern", element, value); 2090 String localeSeparator = getLocaleDisplayPattern("localeSeparator", element, value); 2091 2092 List<String> locales = new ArrayList<>(); 2093 if (element.equals("localePattern")) { 2094 locales.add("uz-AF"); 2095 } 2096 locales.add(element.equals("localeKeyTypePattern") ? "uz-Arab-u-tz-etadd" : "uz-Arab-AF"); 2097 locales.add("uz-Arab-AF-u-tz-etadd-nu-arab"); 2098 String[] examples = new String[locales.size()]; 2099 for (int i = 0; i < locales.size(); i++) { 2100 examples[i] = invertBackground(cldrFile.getName(locales.get(i), false, 2101 localeKeyTypePattern, localePattern, localeSeparator)); 2102 } 2103 result = formatExampleList(examples); 2104 } else if (parts.contains("languages") || parts.contains("scripts") || parts.contains("territories")) { 2105 //ldml/localeDisplayNames/languages/language[@type="ar"] 2106 //ldml/localeDisplayNames/scripts/script[@type="Arab"] 2107 //ldml/localeDisplayNames/territories/territory[@type="CA"] 2108 String type = parts.getAttributeValue(-1, "type"); 2109 if (type.contains("_")) { 2110 if (value != null && !value.equals(type)) { 2111 result = value; 2112 } else { 2113 result = cldrFile.getBaileyValue(xpath, null, null); 2114 } 2115 } else { 2116 value = setBackground(value); 2117 List<String> examples = new ArrayList<>(); 2118 String nameType = parts.getElement(3); 2119 2120 Map<String, String> likely = supplementalDataInfo.getLikelySubtags(); 2121 String alt = parts.getAttributeValue(-1, "alt"); 2122 boolean isStandAloneValue = "stand-alone".equals(alt); 2123 if (!isStandAloneValue) { 2124 // only do this if the value is not a stand-alone form 2125 String tag = "language".equals(nameType) ? type : "und_" + type; 2126 String max = LikelySubtags.maximize(tag, likely); 2127 if (max == null) { 2128 return null; 2129 } 2130 LanguageTagParser ltp = new LanguageTagParser().set(max); 2131 String languageName = null; 2132 String scriptName = null; 2133 String territoryName = null; 2134 if (nameType.equals("language")) { 2135 languageName = value; 2136 } else if (nameType.equals("script")) { 2137 scriptName = value; 2138 } else { 2139 territoryName = value; 2140 } 2141 if (languageName == null) { 2142 languageName = cldrFile.getStringValueWithBailey(CLDRFile.getKey(CLDRFile.LANGUAGE_NAME, ltp.getLanguage())); 2143 if (languageName == null) { 2144 languageName = cldrFile.getStringValueWithBailey(CLDRFile.getKey(CLDRFile.LANGUAGE_NAME, "en")); 2145 } 2146 if (languageName == null) { 2147 languageName = ltp.getLanguage(); 2148 } 2149 } 2150 if (scriptName == null) { 2151 scriptName = cldrFile.getStringValueWithBailey(CLDRFile.getKey(CLDRFile.SCRIPT_NAME, ltp.getScript())); 2152 if (scriptName == null) { 2153 scriptName = cldrFile.getStringValueWithBailey(CLDRFile.getKey(CLDRFile.SCRIPT_NAME, "Latn")); 2154 } 2155 if (scriptName == null) { 2156 scriptName = ltp.getScript(); 2157 } 2158 } 2159 if (territoryName == null) { 2160 territoryName = cldrFile.getStringValueWithBailey(CLDRFile.getKey(CLDRFile.TERRITORY_NAME, ltp.getRegion())); 2161 if (territoryName == null) { 2162 territoryName = cldrFile.getStringValueWithBailey(CLDRFile.getKey(CLDRFile.TERRITORY_NAME, "US")); 2163 } 2164 if (territoryName == null) { 2165 territoryName = ltp.getRegion(); 2166 } 2167 } 2168 languageName = languageName.replace('(', '[').replace(')', ']').replace('(', '[').replace(')', ']'); 2169 scriptName = scriptName.replace('(', '[').replace(')', ']').replace('(', '[').replace(')', ']'); 2170 territoryName = territoryName.replace('(', '[').replace(')', ']').replace('(', '[').replace(')', ']'); 2171 2172 String localePattern = cldrFile.getStringValueWithBailey("//ldml/localeDisplayNames/localeDisplayPattern/localePattern"); 2173 String localeSeparator = cldrFile.getStringValueWithBailey("//ldml/localeDisplayNames/localeDisplayPattern/localeSeparator"); 2174 String scriptTerritory = format(localeSeparator, scriptName, territoryName); 2175 if (!nameType.equals("script")) { 2176 examples.add(invertBackground(format(localePattern, languageName, territoryName))); 2177 } 2178 if (!nameType.equals("territory")) { 2179 examples.add(invertBackground(format(localePattern, languageName, scriptName))); 2180 } 2181 examples.add(invertBackground(format(localePattern, languageName, scriptTerritory))); 2182 } 2183 Output<String> pathWhereFound = null; 2184 if (isStandAloneValue 2185 || cldrFile.getStringValueWithBailey(xpath + ALT_STAND_ALONE, pathWhereFound = new Output<>(), null) == null 2186 || !pathWhereFound.value.contains(ALT_STAND_ALONE)) { 2187 // only do this if either it is a stand-alone form, 2188 // or it isn't and there is no separate stand-alone form 2189 // the extra check after the == null is to make sure that we don't have sideways inheritance 2190 String codePattern = cldrFile.getStringValueWithBailey("//ldml/localeDisplayNames/codePatterns/codePattern[@type=\"" + nameType + "\"]"); 2191 examples.add(invertBackground(format(codePattern, value))); 2192 } 2193 result = formatExampleList(examples.toArray(new String[examples.size()])); 2194 } 2195 } 2196 return result; 2197 } 2198 formatExampleList(String[] examples)2199 private String formatExampleList(String[] examples) { 2200 String result = examples[0]; 2201 for (int i = 1, len = examples.length; i < len; i++) { 2202 result = addExampleResult(examples[i], result); 2203 } 2204 return result; 2205 } 2206 2207 /** 2208 * Return examples formatted as string, with null returned for null or empty examples. 2209 * @param examples 2210 * @return 2211 */ formatExampleList(Collection<String> examples)2212 private String formatExampleList(Collection<String> examples) { 2213 if (examples == null || examples.isEmpty()) { 2214 return null; 2215 } 2216 String result = ""; 2217 boolean first = true; 2218 for (String example : examples) { 2219 if (first) { 2220 result = example; 2221 first = false; 2222 } else { 2223 result = addExampleResult(example, result); 2224 } 2225 } 2226 return result; 2227 } 2228 format(String format, Object... objects)2229 public static String format(String format, Object... objects) { 2230 if (format == null) return null; 2231 return MessageFormat.format(format, objects); 2232 } 2233 unchainException(Exception e)2234 public static final String unchainException(Exception e) { 2235 String stackStr = "[unknown stack]<br>"; 2236 try { 2237 StringWriter asString = new StringWriter(); 2238 e.printStackTrace(new PrintWriter(asString)); 2239 stackStr = "<pre>" + asString.toString() + "</pre>"; 2240 } catch (Throwable tt) { 2241 // ... 2242 } 2243 return stackStr; 2244 } 2245 2246 /** 2247 * Put a background on an item, skipping enclosed patterns. 2248 * @param sampleTerritory 2249 * @return 2250 */ setBackground(String inputPattern)2251 private String setBackground(String inputPattern) { 2252 if (inputPattern == null) { 2253 return "?"; 2254 } 2255 Matcher m = PARAMETER.matcher(inputPattern); 2256 return backgroundStartSymbol + m.replaceAll(backgroundEndSymbol + "$1" + backgroundStartSymbol) 2257 + backgroundEndSymbol; 2258 } 2259 2260 /** 2261 * Put a background on an item, skipping enclosed patterns, except for {0} 2262 * @param patternToEmbed 2263 * @param sampleTerritory 2264 * @return 2265 */ setBackgroundExceptMatch(String input, Pattern patternToEmbed)2266 private String setBackgroundExceptMatch(String input, Pattern patternToEmbed) { 2267 Matcher m = patternToEmbed.matcher(input); 2268 return backgroundStartSymbol + m.replaceAll(backgroundEndSymbol + "$1" + backgroundStartSymbol) 2269 + backgroundEndSymbol; 2270 } 2271 2272 /** 2273 * Put a background on an item, skipping enclosed patterns, except for {0} 2274 * 2275 * @param patternToEmbed 2276 * TODO 2277 * @param sampleTerritory 2278 * 2279 * @return 2280 */ setBackgroundOnMatch(String inputPattern, Pattern patternToEmbed)2281 private String setBackgroundOnMatch(String inputPattern, Pattern patternToEmbed) { 2282 Matcher m = patternToEmbed.matcher(inputPattern); 2283 return m.replaceAll(backgroundStartSymbol + "$1" + backgroundEndSymbol); 2284 } 2285 2286 /** 2287 * This adds the transliteration of a result in case it has one (i.e. sr_Cyrl -> sr_Latn). 2288 * 2289 * @param input 2290 * string with special characters from setBackground. 2291 * @param value 2292 * value to be transliterated 2293 * @return string with attached transliteration if there is one. 2294 */ addTransliteration(String input, String value)2295 private String addTransliteration(String input, String value) { 2296 if (value == null) { 2297 return input; 2298 } 2299 for (LocaleTransform localeTransform : LocaleTransform.values()) { 2300 2301 String locale = cldrFile.getLocaleID(); 2302 2303 if (!(localeTransform.getInputLocale().equals(locale))) { 2304 continue; 2305 } 2306 2307 Factory factory = CONFIG.getCldrFactory(); 2308 CLDRFileTransformer transformer = new CLDRFileTransformer(factory, CLDRPaths.COMMON_DIRECTORY + "transforms/"); 2309 Transliterator transliterator = transformer.loadTransliterator(localeTransform); 2310 final String transliterated = transliterator.transliterate(value); 2311 if (!transliterated.equals(value)) { 2312 return backgroundStartSymbol + "[ " + transliterated + " ]" + backgroundEndSymbol + exampleSeparatorSymbol + input; 2313 } 2314 } 2315 return input; 2316 } 2317 2318 /** 2319 * This is called just before we return a result. It fixes the special characters that were added by setBackground. 2320 * 2321 * @param input string with special characters from setBackground. 2322 * @param invert 2323 * @return string with HTML for the background. 2324 */ finalizeBackground(String input)2325 private String finalizeBackground(String input) { 2326 if (input == null) { 2327 return input; 2328 } 2329 String coreString = 2330 TransliteratorUtilities.toHTML.transliterate(input) 2331 .replace(backgroundStartSymbol + backgroundEndSymbol, "") 2332 // remove null runs 2333 .replace(backgroundEndSymbol + backgroundStartSymbol, "") 2334 // remove null runs 2335 .replace(backgroundStartSymbol, backgroundStart) 2336 .replace(backgroundEndSymbol, backgroundEnd) 2337 .replace(backgroundAutoStartSymbol, backgroundAutoStart) 2338 .replace(backgroundAutoEndSymbol, backgroundAutoEnd) 2339 .replace(exampleSeparatorSymbol, exampleEnd + exampleStart) 2340 .replace(exampleStartAutoSymbol, exampleStartAuto) 2341 .replace(exampleStartRTLSymbol, exampleStartRTL) 2342 .replace(exampleStartHeaderSymbol, exampleStartHeader) 2343 .replace(exampleEndSymbol, exampleEnd) 2344 .replace(startItalicSymbol, startItalic) 2345 .replace(endItalicSymbol, endItalic) 2346 .replace(startSupSymbol, startSup) 2347 .replace(endSupSymbol, endSup) 2348 ; 2349 // If we are not showing context, we use exampleSeparatorSymbol between examples, 2350 // and then need to add the initial exampleStart and final exampleEnd. 2351 return (input.indexOf(exampleStartAutoSymbol) >= 0)? coreString: 2352 exampleStart + coreString + exampleEnd; 2353 } 2354 invertBackground(String input)2355 private String invertBackground(String input) { 2356 return input == null ? null 2357 : backgroundStartSymbol 2358 + input.replace(backgroundStartSymbol, backgroundTempSymbol) 2359 .replace(backgroundEndSymbol, backgroundStartSymbol) 2360 .replace(backgroundTempSymbol, backgroundEndSymbol) 2361 + backgroundEndSymbol; 2362 } 2363 removeEmptyRuns(String input)2364 private String removeEmptyRuns(String input) { 2365 return input.replace(backgroundStartSymbol + backgroundEndSymbol, "") 2366 .replace(backgroundEndSymbol + backgroundStartSymbol, ""); 2367 } 2368 2369 /** 2370 * Utility to format using a gmtHourString, gmtFormat, and an integer hours. We only need the hours because that's 2371 * all 2372 * the TZDB IDs need. Should merge this eventually into TimeZoneFormatter and call there. 2373 * 2374 * @param gmtHourString 2375 * @param gmtFormat 2376 * @param hours 2377 * @return 2378 */ getGMTFormat(String gmtHourString, String gmtFormat, int hours)2379 private String getGMTFormat(String gmtHourString, String gmtFormat, int hours) { 2380 return getGMTFormat(gmtHourString, gmtFormat, hours, 0); 2381 } 2382 getGMTFormat(String gmtHourString, String gmtFormat, int hours, int minutes)2383 private String getGMTFormat(String gmtHourString, String gmtFormat, int hours, int minutes) { 2384 boolean hoursBackground = false; 2385 if (gmtHourString == null) { 2386 hoursBackground = true; 2387 gmtHourString = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/hourFormat"); 2388 } 2389 if (gmtFormat == null) { 2390 hoursBackground = false; // for the hours case 2391 gmtFormat = setBackground(cldrFile.getWinningValue("//ldml/dates/timeZoneNames/gmtFormat")); 2392 } 2393 String[] plusMinus = gmtHourString.split(";"); 2394 2395 SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian", plusMinus[hours >= 0 ? 0 : 1]); 2396 dateFormat.setTimeZone(ZONE_SAMPLE); 2397 calendar.set(1999, 9, 27, Math.abs(hours), minutes, 0); // 1999-09-13 13:25:59 2398 Date sample = calendar.getTime(); 2399 String hourString = dateFormat.format(sample); 2400 if (hoursBackground) { 2401 hourString = setBackground(hourString); 2402 } 2403 String result = format(gmtFormat, hourString); 2404 return result; 2405 } 2406 getMZTimeFormat()2407 private String getMZTimeFormat() { 2408 String timeFormat = cldrFile 2409 .getWinningValue( 2410 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/timeFormats/timeFormatLength[@type=\"short\"]/timeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]"); 2411 if (timeFormat == null) { 2412 timeFormat = "HH:mm"; 2413 } 2414 // the following is <= because the TZDB inverts the hours 2415 SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian", timeFormat); 2416 dateFormat.setTimeZone(ZONE_SAMPLE); 2417 calendar.set(1999, 9, 13, 13, 25, 59); // 1999-09-13 13:25:59 2418 Date sample = calendar.getTime(); 2419 String result = dateFormat.format(sample); 2420 return result; 2421 } 2422 2423 /** 2424 * Return a help string, in html, that should be shown in the Zoomed view. 2425 * Presumably at the end of each help section is something like: <br> 2426 * <br>For more information, see <a 2427 * href='http://unicode.org/cldr/wiki?SurveyToolHelp/characters'>help</a>. <br> 2428 * The result is valid HTML. Set listPlaceholders to true to include a 2429 * HTML-formatted table of all placeholders required in the value.<br> 2430 * TODO: add more help, and modify to get from property or xml file for easy 2431 * modification. 2432 * 2433 * @return null if none available. 2434 */ getHelpHtml(String xpath, String value, boolean listPlaceholders)2435 public synchronized String getHelpHtml(String xpath, String value, boolean listPlaceholders) { 2436 2437 // lazy initialization 2438 2439 if (pathDescription == null) { 2440 Map<String, List<Set<String>>> starredPaths = new HashMap<>(); 2441 Map<String, String> extras = new HashMap<>(); 2442 2443 this.pathDescription = new PathDescription(supplementalDataInfo, englishFile, extras, starredPaths, 2444 PathDescription.ErrorHandling.CONTINUE); 2445 2446 if (helpMessages == null) { 2447 helpMessages = new HelpMessages("test_help_messages.html"); 2448 } 2449 } 2450 2451 // now get the description 2452 2453 Level level = CONFIG.getCoverageInfo().getCoverageLevel(xpath, cldrFile.getLocaleID()); 2454 String description = pathDescription.getDescription(xpath, value, level, null); 2455 if (description == null || description.equals("SKIP")) { 2456 return null; 2457 } 2458 int start = 0; 2459 StringBuilder buffer = new StringBuilder(); 2460 2461 Matcher URLMatcher = URL_PATTERN.matcher(""); 2462 while (URLMatcher.reset(description).find(start)) { 2463 final String url = URLMatcher.group(); 2464 buffer 2465 .append(TransliteratorUtilities.toHTML.transliterate(description.substring(start, URLMatcher.start()))) 2466 .append("<a target='CLDR-ST-DOCS' href='") 2467 .append(url) 2468 .append("'>") 2469 .append(url) 2470 .append("</a>"); 2471 start = URLMatcher.end(); 2472 } 2473 buffer.append(TransliteratorUtilities.toHTML.transliterate(description.substring(start))); 2474 2475 if (listPlaceholders) { 2476 buffer.append(pathDescription.getPlaceholderDescription(xpath)); 2477 } 2478 if (AnnotationUtil.pathIsAnnotation(xpath)) { 2479 XPathParts emoji = XPathParts.getFrozenInstance(xpath); 2480 String cp = emoji.getAttributeValue(-1, "cp"); 2481 String minimal = Utility.hex(cp.replace("", "")).replace(',', '_').toLowerCase(Locale.ROOT); 2482 buffer.append("<br><img height='64px' width='auto' src='images/emoji/emoji_" + minimal + ".png'>"); 2483 } 2484 2485 return buffer.toString(); 2486 } 2487 getHelpHtml(String xpath, String value)2488 public synchronized String getHelpHtml(String xpath, String value) { 2489 return getHelpHtml(xpath, value, false); 2490 } 2491 simplify(String exampleHtml)2492 public static String simplify(String exampleHtml) { 2493 return simplify(exampleHtml, false); 2494 } 2495 simplify(String exampleHtml, boolean internal)2496 public static String simplify(String exampleHtml, boolean internal) { 2497 if (exampleHtml == null) { 2498 return null; 2499 } 2500 if (internal) { 2501 return "〖" 2502 + exampleHtml 2503 .replace(backgroundStartSymbol, "❬") 2504 .replace(backgroundEndSymbol, "❭") 2505 + "〗"; 2506 } 2507 int startIndex = exampleHtml.indexOf(exampleStartHeader); 2508 if (startIndex >= 0) { 2509 int endIndex = exampleHtml.indexOf(exampleEnd, startIndex); 2510 if (endIndex > startIndex) { 2511 // remove header for context examples 2512 endIndex += exampleEnd.length(); 2513 String head = exampleHtml.substring(0,startIndex); 2514 String tail = exampleHtml.substring(endIndex); 2515 exampleHtml = head + tail; 2516 } 2517 } 2518 return exampleHtml 2519 .replace("<div class='cldr_example'>", "〖") 2520 .replace("<div class='cldr_example_auto' dir='auto'>", "【") 2521 .replace("<div class='cldr_example_rtl' dir='rtl'>", "【⃪") 2522 .replace("</div>", "〗") 2523 .replace("<span class='cldr_substituted'>", "❬") 2524 .replace("</span>", "❭"); 2525 } 2526 } 2527