1 package org.unicode.cldr.test; 2 3 import com.google.common.base.Joiner; 4 import com.google.common.collect.ImmutableList; 5 import com.ibm.icu.impl.Row.R3; 6 import com.ibm.icu.impl.Utility; 7 import com.ibm.icu.impl.number.DecimalQuantity; 8 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; 9 import com.ibm.icu.lang.UCharacter; 10 import com.ibm.icu.text.BreakIterator; 11 import com.ibm.icu.text.DateFormat; 12 import com.ibm.icu.text.DateFormatSymbols; 13 import com.ibm.icu.text.DateTimePatternGenerator; 14 import com.ibm.icu.text.DecimalFormat; 15 import com.ibm.icu.text.DecimalFormatSymbols; 16 import com.ibm.icu.text.ListFormatter; 17 import com.ibm.icu.text.MessageFormat; 18 import com.ibm.icu.text.NumberFormat; 19 import com.ibm.icu.text.PluralRules; 20 import com.ibm.icu.text.PluralRules.DecimalQuantitySamples; 21 import com.ibm.icu.text.PluralRules.DecimalQuantitySamplesRange; 22 import com.ibm.icu.text.PluralRules.Operand; 23 import com.ibm.icu.text.PluralRules.SampleType; 24 import com.ibm.icu.text.SimpleDateFormat; 25 import com.ibm.icu.text.SimpleFormatter; 26 import com.ibm.icu.text.UTF16; 27 import com.ibm.icu.text.UnicodeSet; 28 import com.ibm.icu.util.Calendar; 29 import com.ibm.icu.util.GregorianCalendar; 30 import com.ibm.icu.util.Output; 31 import com.ibm.icu.util.TimeZone; 32 import com.ibm.icu.util.ULocale; 33 import java.io.PrintWriter; 34 import java.io.StringWriter; 35 import java.text.ChoiceFormat; 36 import java.util.ArrayList; 37 import java.util.BitSet; 38 import java.util.Collection; 39 import java.util.Date; 40 import java.util.HashMap; 41 import java.util.LinkedHashSet; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.Map.Entry; 46 import java.util.Objects; 47 import java.util.Set; 48 import java.util.function.Function; 49 import java.util.regex.Matcher; 50 import java.util.regex.Pattern; 51 import org.unicode.cldr.tool.LikelySubtags; 52 import org.unicode.cldr.util.AnnotationUtil; 53 import org.unicode.cldr.util.CLDRConfig; 54 import org.unicode.cldr.util.CLDRFile; 55 import org.unicode.cldr.util.CLDRFile.ExemplarType; 56 import org.unicode.cldr.util.CLDRFile.WinningChoice; 57 import org.unicode.cldr.util.CLDRLocale; 58 import org.unicode.cldr.util.CldrUtility; 59 import org.unicode.cldr.util.CodePointEscaper; 60 import org.unicode.cldr.util.DateConstants; 61 import org.unicode.cldr.util.DayPeriodInfo; 62 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod; 63 import org.unicode.cldr.util.EmojiConstants; 64 import org.unicode.cldr.util.GrammarInfo; 65 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature; 66 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope; 67 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget; 68 import org.unicode.cldr.util.ICUServiceBuilder; 69 import org.unicode.cldr.util.LanguageTagParser; 70 import org.unicode.cldr.util.Level; 71 import org.unicode.cldr.util.PathDescription; 72 import org.unicode.cldr.util.PatternCache; 73 import org.unicode.cldr.util.PluralSamples; 74 import org.unicode.cldr.util.Rational; 75 import org.unicode.cldr.util.Rational.FormatStyle; 76 import org.unicode.cldr.util.ScriptToExemplars; 77 import org.unicode.cldr.util.SimpleUnicodeSetFormatter; 78 import org.unicode.cldr.util.SupplementalDataInfo; 79 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo; 80 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count; 81 import org.unicode.cldr.util.SupplementalDataInfo.PluralType; 82 import org.unicode.cldr.util.TransliteratorUtilities; 83 import org.unicode.cldr.util.UnitConverter; 84 import org.unicode.cldr.util.UnitConverter.UnitSystem; 85 import org.unicode.cldr.util.Units; 86 import org.unicode.cldr.util.XListFormatter.ListTypeLength; 87 import org.unicode.cldr.util.XPathParts; 88 import org.unicode.cldr.util.personname.PersonNameFormatter; 89 import org.unicode.cldr.util.personname.PersonNameFormatter.FallbackFormatter; 90 import org.unicode.cldr.util.personname.PersonNameFormatter.FormatParameters; 91 import org.unicode.cldr.util.personname.PersonNameFormatter.NameObject; 92 import org.unicode.cldr.util.personname.PersonNameFormatter.NamePattern; 93 import org.unicode.cldr.util.personname.SimpleNameObject; 94 95 /** 96 * Class to generate examples and help messages for the Survey tool (or console version). 97 * 98 * @author markdavis 99 */ 100 public class ExampleGenerator { 101 private static final String INTERNAL = "internal: "; 102 private static final String SUBTRACTS = "➖"; 103 private static final String ADDS = "➕"; 104 private static final String HINTS = "️"; 105 private static final String EXAMPLE_OF_INCORRECT = "❌ "; 106 private static final String EXAMPLE_OF_CAUTION = "⚠️ "; 107 108 private static final boolean DEBUG_EXAMPLE_GENERATOR = false; 109 110 static final boolean DEBUG_SHOW_HELP = false; 111 112 private static final CLDRConfig CONFIG = CLDRConfig.getInstance(); 113 114 private static final String ALT_STAND_ALONE = "[@alt=\"stand-alone\"]"; 115 116 private static final String EXEMPLAR_CITY_LOS_ANGELES = 117 "//ldml/dates/timeZoneNames/zone[@type=\"America/Los_Angeles\"]/exemplarCity"; 118 119 private static final Pattern URL_PATTERN = 120 Pattern.compile("http://[\\-a-zA-Z0-9]+(\\.[\\-a-zA-Z0-9]+)*([/#][\\-a-zA-Z0-9]+)*"); 121 122 private static final SupplementalDataInfo supplementalDataInfo = 123 SupplementalDataInfo.getInstance(); 124 static final UnitConverter UNIT_CONVERTER = supplementalDataInfo.getUnitConverter(); 125 126 public static final double NUMBER_SAMPLE = 123456.789; 127 public static final double NUMBER_SAMPLE_WHOLE = 2345; 128 129 public static final TimeZone ZONE_SAMPLE = TimeZone.getTimeZone("America/Indianapolis"); 130 public static final TimeZone GMT_ZONE_SAMPLE = TimeZone.getTimeZone("Etc/GMT"); 131 132 private static final String exampleStart = "<div class='cldr_example'>"; 133 private static final String exampleStartAuto = "<div class='cldr_example_auto' dir='auto'>"; 134 private static final String exampleStartRTL = "<div class='cldr_example_rtl' dir='rtl'>"; 135 private static final String exampleStartHeader = "<div class='cldr_example_rtl'>"; 136 private static final String exampleEnd = "</div>"; 137 private static final String startItalic = "<i>"; 138 private static final String endItalic = "</i>"; 139 private static final String startSup = "<sup>"; 140 private static final String endSup = "</sup>"; 141 private static final String backgroundAutoStart = "<span class='cldr_background_auto'>"; 142 private static final String backgroundAutoEnd = "</span>"; 143 private String backgroundStart = "<span class='cldr_substituted'>"; // overrideable 144 private String backgroundEnd = "</span>"; // overrideable 145 146 public static final String backgroundStartSymbol = "\uE234"; 147 public static final String backgroundEndSymbol = "\uE235"; 148 private static final String backgroundTempSymbol = "\uE236"; 149 private static final String exampleSeparatorSymbol = "\uE237"; 150 private static final String startItalicSymbol = "\uE238"; 151 private static final String endItalicSymbol = "\uE239"; 152 private static final String startSupSymbol = "\uE23A"; 153 private static final String endSupSymbol = "\uE23B"; 154 private static final String backgroundAutoStartSymbol = "\uE23C"; 155 private static final String backgroundAutoEndSymbol = "\uE23D"; 156 private static final String exampleStartAutoSymbol = "\uE23E"; 157 private static final String exampleStartRTLSymbol = "\uE23F"; 158 private static final String exampleStartHeaderSymbol = "\uE240"; 159 private static final String exampleEndSymbol = "\uE241"; 160 161 private static final String contextheader = 162 "Key: " + backgroundAutoStartSymbol + "neutral" + backgroundAutoEndSymbol + ", RTL"; 163 164 public static final char TEXT_VARIANT = '\uFE0E'; 165 166 private static final UnicodeSet BIDI_MARKS = new UnicodeSet("[:Bidi_Control:]").freeze(); 167 168 public static final Date DATE_SAMPLE; 169 170 private static final Date DATE_SAMPLE2; 171 private static final Date DATE_SAMPLE3; 172 private static final Date DATE_SAMPLE4; 173 private static final Date DATE_SAMPLE5; 174 175 static { 176 Calendar calendar = Calendar.getInstance(ZONE_SAMPLE, ULocale.ENGLISH); 177 calendar.set( 178 1999, 8, 5, 13, 25, 59); // 1999-09-05 13:25:59 // calendar.set month is 0 based 179 DATE_SAMPLE = calendar.getTime(); 180 calendar.set(1999, 9, 27, 13, 25, 59); // 1999-10-27 13:25:59 181 DATE_SAMPLE2 = calendar.getTime(); 182 calendar.set(1999, 8, 5, 7, 0, 0); // 1999-09-05 07:00:00 183 DATE_SAMPLE3 = calendar.getTime(); 184 calendar.set(1999, 8, 5, 23, 0, 0); // 1999-09-05 23:00:00 185 DATE_SAMPLE4 = calendar.getTime(); 186 187 calendar.set(1999, 8, 5, 3, 25, 59); // 1999-09-05 03:25:59 188 DATE_SAMPLE5 = calendar.getTime(); 189 } 190 191 static final List<DecimalQuantity> CURRENCY_SAMPLES = 192 ImmutableList.of( 193 DecimalQuantity_DualStorageBCD.fromExponentString("1.23"), 194 DecimalQuantity_DualStorageBCD.fromExponentString("0"), 195 DecimalQuantity_DualStorageBCD.fromExponentString("2.34"), 196 DecimalQuantity_DualStorageBCD.fromExponentString("3.45"), 197 DecimalQuantity_DualStorageBCD.fromExponentString("5.67"), 198 DecimalQuantity_DualStorageBCD.fromExponentString("1")); 199 200 public static final Pattern PARAMETER = PatternCache.get("(\\{(?:0|[1-9][0-9]*)\\})"); 201 public static final Pattern PARAMETER_SKIP0 = PatternCache.get("(\\{[1-9][0-9]*\\})"); 202 public static final Pattern ALL_DIGITS = PatternCache.get("(\\p{Nd}+(.\\p{Nd}+)?)"); 203 204 private static final Calendar generatingCalendar = Calendar.getInstance(ULocale.US); 205 getDate(int year, int month, int date, int hour, int minute, int second)206 private static Date getDate(int year, int month, int date, int hour, int minute, int second) { 207 synchronized (generatingCalendar) { 208 generatingCalendar.setTimeZone(GMT_ZONE_SAMPLE); 209 generatingCalendar.set(year, month, date, hour, minute, second); 210 return generatingCalendar.getTime(); 211 } 212 } 213 214 private static final Date FIRST_INTERVAL = getDate(2008, 1, 13, 5, 7, 9); 215 private static final Map<String, Date> SECOND_INTERVAL = 216 CldrUtility.asMap( 217 new Object[][] { 218 { 219 "G", getDate(1009, 2, 14, 17, 8, 10) 220 }, // "G" mostly useful for calendars that have short eras, like Japanese 221 {"y", getDate(2009, 2, 14, 17, 8, 10)}, 222 {"M", getDate(2008, 2, 14, 17, 8, 10)}, 223 {"d", getDate(2008, 1, 14, 17, 8, 10)}, 224 {"a", getDate(2008, 1, 13, 17, 8, 10)}, 225 {"h", getDate(2008, 1, 13, 6, 8, 10)}, 226 {"m", getDate(2008, 1, 13, 5, 8, 10)} 227 }); 228 setCachingEnabled(boolean enabled)229 public void setCachingEnabled(boolean enabled) { 230 exCache.setCachingEnabled(enabled); 231 icuServiceBuilder.setCachingEnabled(enabled); 232 } 233 234 /** 235 * verboseErrors affects not only the verboseness of error reporting, but also, for example, 236 * whether some unit tests pass or fail. The function setVerboseErrors can be used to modify it. 237 * It must be initialized here to false, otherwise cldr-unittest TestAll.java fails. Reference: 238 * https://unicode.org/cldr/trac/ticket/12025 239 */ 240 private boolean verboseErrors = false; 241 242 private final Calendar calendar = Calendar.getInstance(ZONE_SAMPLE, ULocale.ENGLISH); 243 244 private final CLDRFile cldrFile; 245 246 private final CLDRFile englishFile; 247 private CLDRFile cyrillicFile; 248 private CLDRFile japanFile; 249 250 private final BestMinimalPairSamples bestMinimalPairSamples; 251 252 private final ExampleCache exCache = new ExampleCache(); 253 254 private final ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder(); 255 256 private final PluralInfo pluralInfo; 257 258 private final GrammarInfo grammarInfo; 259 260 private PluralSamples patternExamples; 261 262 private final Map<String, String> subdivisionIdToName; 263 264 private String creationTime = null; // only used if DEBUG_EXAMPLE_GENERATOR 265 266 private final IntervalFormat intervalFormat = new IntervalFormat(); 267 268 private PathDescription pathDescription; 269 270 /** 271 * True if this ExampleGenerator is especially for generating "English" examples, false if it is 272 * for generating "native" examples. 273 */ 274 private final boolean typeIsEnglish; 275 276 /** True if this ExampleGenerator is for RTL locale. */ 277 private final boolean isRTL; 278 279 HelpMessages helpMessages; 280 281 /* For each calendar type, maps the closest two eras to 2025 282 * defined in that calendar to their corresponding start/end date. 283 * Dates are adjusted to be 2 days after official era start date and 284 * 2 days before era end date to avoid time zone issues. 285 * TODO: include methods for calendarData in supplementalDataInfo API 286 * to extract this data directly from supplementaldata.xml 287 */ 288 public static final Map<String, List<Date>> CALENDAR_ERAS = 289 new HashMap<String, List<Date>>() { 290 { // month 0-indexed. start/end days adjusted by +/- 2, respectively 291 put( 292 "gregorian", 293 List.of( 294 new GregorianCalendar(0, 11, 29).getTime(), 295 new GregorianCalendar(1, 0, 03).getTime())); 296 put( 297 "japanese", 298 List.of( 299 new GregorianCalendar(1989, 0, 10).getTime(), 300 new GregorianCalendar(2019, 4, 3).getTime())); 301 put("islamic", List.of(new GregorianCalendar(622, 6, 17).getTime())); 302 put("chinese", List.of(new GregorianCalendar(-2636, 0, 03).getTime())); 303 put("hebrew", List.of(new GregorianCalendar(-3760, 9, 9).getTime())); 304 put("buddhist", List.of(new GregorianCalendar(-542, 0, 03).getTime())); 305 put( 306 "coptic", 307 List.of( 308 new GregorianCalendar(284, 07, 26).getTime(), 309 new GregorianCalendar(284, 07, 31).getTime())); 310 put("persian", List.of(new GregorianCalendar(622, 0, 03).getTime())); 311 put("dangi", List.of(new GregorianCalendar(-2332, 0, 03).getTime())); 312 put( 313 "ethiopic", 314 List.of( 315 new GregorianCalendar(8, 07, 26).getTime(), 316 new GregorianCalendar(8, 07, 31).getTime())); 317 put( 318 "ethiopic-amete-alem", 319 List.of(new GregorianCalendar(-5492, 07, 27).getTime())); 320 put("indian", List.of(new GregorianCalendar(79, 0, 03).getTime())); 321 put( 322 "roc", 323 List.of( 324 new GregorianCalendar(1911, 11, 29).getTime(), 325 new GregorianCalendar(1912, 0, 03).getTime())); 326 } 327 }; 328 329 // map relativeTimePattern counts to numeric examples 330 public static final Map<String, String> COUNTS = 331 new HashMap<String, String>() { 332 { 333 put("zero", "0"); 334 put("one", "1"); 335 put("two", "2"); 336 put("few", "3"); 337 put("many", "5"); 338 put("other", "10"); 339 } 340 }; 341 getCldrFile()342 public CLDRFile getCldrFile() { 343 return cldrFile; 344 } 345 346 /** 347 * For this (locale-specific) ExampleGenerator, clear the cached examples for any paths whose 348 * examples might depend on the winning value of the given path, since the winning value of the 349 * given path has changed. 350 * 351 * @param xpath the path whose winning value has changed 352 * <p>Called by TestCache.updateExampleGeneratorCache 353 */ updateCache(String xpath)354 public void updateCache(String xpath) { 355 exCache.update(xpath); 356 if (ICUServiceBuilder.ISB_CAN_CLEAR_CACHE) { 357 icuServiceBuilder.clearCache(); 358 } 359 } 360 361 /** 362 * For getting the end of the "background" style. Default is "</span>". It is used in composing 363 * patterns, so it can show the part that corresponds to the value. 364 * 365 * @return 366 */ getBackgroundEnd()367 public String getBackgroundEnd() { 368 return backgroundEnd; 369 } 370 371 /** 372 * For setting the end of the "background" style. Default is "</span>". It is used in composing 373 * patterns, so it can show the part that corresponds to the value. 374 */ setBackgroundEnd(String backgroundEnd)375 public void setBackgroundEnd(String backgroundEnd) { 376 this.backgroundEnd = backgroundEnd; 377 } 378 379 /** 380 * For getting the "background" style. Default is "<span style='background-color: gray'>". It is 381 * used in composing patterns, so it can show the part that corresponds to the value. 382 * 383 * @return 384 */ getBackgroundStart()385 public String getBackgroundStart() { 386 return backgroundStart; 387 } 388 389 /** 390 * For setting the "background" style. Default is "<span style='background-color: gray'>". It is 391 * used in composing patterns, so it can show the part that corresponds to the value. 392 */ setBackgroundStart(String backgroundStart)393 public void setBackgroundStart(String backgroundStart) { 394 this.backgroundStart = backgroundStart; 395 } 396 397 /** 398 * Set the verbosity level of internal errors. For example, setVerboseErrors(true) will cause 399 * full stack traces to be shown in some cases. 400 */ setVerboseErrors(boolean verbosity)401 public void setVerboseErrors(boolean verbosity) { 402 this.verboseErrors = verbosity; 403 } 404 405 /** 406 * Create an Example Generator. If this is shared across threads, it must be synchronized. 407 * 408 * @param resolvedCldrFile 409 * @param englishFile 410 */ ExampleGenerator(CLDRFile resolvedCldrFile, CLDRFile englishFile)411 public ExampleGenerator(CLDRFile resolvedCldrFile, CLDRFile englishFile) { 412 if (!resolvedCldrFile.isResolved()) { 413 throw new IllegalArgumentException("CLDRFile must be resolved"); 414 } 415 if (!englishFile.isResolved()) { 416 throw new IllegalArgumentException("English CLDRFile must be resolved"); 417 } 418 cldrFile = resolvedCldrFile; 419 final String localeId = cldrFile.getLocaleID(); 420 subdivisionIdToName = EmojiSubdivisionNames.getSubdivisionIdToName(localeId); 421 pluralInfo = supplementalDataInfo.getPlurals(PluralType.cardinal, localeId); 422 grammarInfo = 423 supplementalDataInfo.getGrammarInfo(localeId); // getGrammarInfo can return null 424 this.englishFile = englishFile; 425 this.typeIsEnglish = (resolvedCldrFile == englishFile); 426 icuServiceBuilder.setCldrFile(cldrFile); 427 428 bestMinimalPairSamples = new BestMinimalPairSamples(cldrFile, icuServiceBuilder, false); 429 430 String characterOrder = cldrFile.getStringValue("//ldml/layout/orientation/characterOrder"); 431 this.isRTL = (characterOrder != null && characterOrder.equals("right-to-left")); 432 433 if (DEBUG_EXAMPLE_GENERATOR) { 434 creationTime = 435 new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") 436 .format(Calendar.getInstance().getTime()); 437 System.out.println( 438 " Created new ExampleGenerator for loc " + localeId + " at " + creationTime); 439 } 440 } 441 442 /** 443 * Get an example string, in html, if there is one for this path, otherwise null. For use in the 444 * survey tool, an example might be returned *even* if there is no value in the locale. For 445 * example, the locale might have a path that English doesn't, but you want to return the best 446 * English example. <br> 447 * The result is valid HTML. 448 * 449 * <p>If generating examples for an inheritance marker, use the "real" inherited value to 450 * generate from. Do this BEFORE accessing the cache, which doesn't use INHERITANCE_MARKER. 451 * 452 * @param xpath the path; e.g., "//ldml/dates/timeZoneNames/fallbackFormat" 453 * @param value the value; e.g., "{1} [{0}]"; not necessarily the winning value 454 * @return the example HTML, or null 455 */ getExampleHtml(String xpath, String value)456 public String getExampleHtml(String xpath, String value) { 457 return getExampleHtmlExtended(xpath, value, false /* nonTrivial */); 458 } 459 460 /** 461 * Same as getExampleHtml but return null if the result would simply be the given value plus 462 * some markup 463 * 464 * <p>For example, for path = //ldml/localeDisplayNames/languages/language[@type="nl_BE"] and 465 * value = "Flemish", getExampleHtml returns "<div class='cldr_example'>Flemish</div>", which is 466 * trivial. Maybe there is some context in which such trivial examples are useful -- if not, 467 * getExampleHtml should be revised to be the same as getNonTrivialExampleHtml and there won't 468 * be a need for this distinct method. 469 * 470 * @param xpath the path; e.g., "//ldml/dates/timeZoneNames/fallbackFormat" 471 * @param value the value; e.g., "{1} [{0}]"; not necessarily the winning value 472 * @return the example HTML, or null 473 */ getNonTrivialExampleHtml(String xpath, String value)474 public String getNonTrivialExampleHtml(String xpath, String value) { 475 return getExampleHtmlExtended(xpath, value, true /* nonTrivial */); 476 } 477 getExampleHtmlExtended(String xpath, String value, boolean nonTrivial)478 private String getExampleHtmlExtended(String xpath, String value, boolean nonTrivial) { 479 if (value == null || xpath == null || xpath.endsWith("/alias")) { 480 return null; 481 } 482 String result; 483 try { 484 if (CldrUtility.INHERITANCE_MARKER.equals(value)) { 485 value = cldrFile.getBaileyValue(xpath, null, null); 486 if (value == null) { 487 /* 488 * This can happen for some paths, such as 489 * //ldml/dates/timeZoneNames/metazone[@type="Mawson"]/short/daylight 490 */ 491 return null; 492 } 493 } 494 ExampleCache.ExampleCacheItem cacheItem = exCache.new ExampleCacheItem(xpath, value); 495 result = cacheItem.getExample(); 496 if (result != null) { 497 return result; 498 } 499 result = constructExampleHtml(xpath, value, nonTrivial); 500 cacheItem.putExample(result); 501 } catch (RuntimeException e) { 502 e.printStackTrace(); 503 String unchained = 504 verboseErrors ? ("<br>" + finalizeBackground(unchainException(e))) : ""; 505 result = "<i>Parsing error. " + finalizeBackground(e.getMessage()) + "</i>" + unchained; 506 } 507 return result; 508 } 509 510 /** 511 * Do the main work of getExampleHtml given that the result was not found in the cache. 512 * 513 * <p>Creates a list that the handlers in constructExampleHtmlExtended can add examples to, and 514 * then formats the example list appropriately. 515 * 516 * @param xpath the path; e.g., "//ldml/dates/timeZoneNames/fallbackFormat" 517 * @param value the value; e.g., "{1} [{0}]"; not necessarily the winning value 518 * @param nonTrivial true if we should avoid returning a trivial example (just value wrapped in 519 * markup) 520 * @return the example HTML, or null 521 */ constructExampleHtml(String xpath, String value, boolean nonTrivial)522 private String constructExampleHtml(String xpath, String value, boolean nonTrivial) { 523 List<String> examples = new ArrayList<>(); 524 constructExampleHtmlExtended(xpath, value, examples); 525 String result = formatExampleList(examples); 526 if (result != null) { // Handle the outcome 527 if (nonTrivial && value.equals(result)) { 528 result = null; 529 } else { 530 result = finalizeBackground(result); 531 } 532 } 533 return result; 534 } 535 constructExampleHtmlExtended(String xpath, String value, List<String> examples)536 private void constructExampleHtmlExtended(String xpath, String value, List<String> examples) { 537 boolean showContexts = 538 isRTL || BIDI_MARKS.containsSome(value); // only used for certain example types 539 /* 540 * Need getInstance, not getFrozenInstance here: some functions such as handleNumberSymbol 541 * expect to call functions like parts.addRelative which throw exceptions if parts is frozen. 542 */ 543 XPathParts parts = XPathParts.getFrozenInstance(xpath).cloneAsThawed(); 544 if (parts.contains("dateRangePattern")) { // {0} - {1} 545 handleDateRangePattern(value, examples); 546 } else if (parts.contains("timeZoneNames")) { 547 handleTimeZoneName(parts, value, examples); 548 } else if (parts.contains("localeDisplayNames")) { 549 handleDisplayNames(xpath, parts, value, examples); 550 } else if (parts.contains("currency")) { 551 handleCurrency(xpath, parts, value, examples); 552 } else if (parts.contains("eras")) { 553 handleEras(parts, value, examples); 554 } else if (parts.contains("quarters")) { 555 handleQuarters(parts, value, examples); 556 } else if (parts.contains("relative") 557 || parts.contains("relativeTime") 558 || parts.contains("relativePeriod")) { 559 handleRelative(xpath, parts, value, examples); 560 } else if (parts.contains("dayPeriods")) { 561 handleDayPeriod(parts, value, examples); 562 } else if (parts.contains("monthContext")) { 563 handleDateSymbol(parts, value, examples); 564 } else if (parts.contains("pattern") || parts.contains("dateFormatItem")) { 565 if (parts.contains("calendar")) { 566 handleDateFormatItem(xpath, value, showContexts, examples); 567 } else if (parts.contains("miscPatterns")) { 568 handleMiscPatterns(parts, value, examples); 569 } else if (parts.contains("numbers")) { 570 if (parts.contains("currencyFormat")) { 571 handleCurrencyFormat(parts, value, showContexts, examples); 572 } else { 573 handleDecimalFormat(parts, value, showContexts, examples); 574 } 575 } 576 } else if (parts.contains("minimumGroupingDigits")) { 577 handleMinimumGrouping(parts, value, examples); 578 } else if (parts.getElement(2).contains("symbols")) { 579 handleNumberSymbol(parts, value, examples); 580 } else if (parts.contains("defaultNumberingSystem") 581 || parts.contains("otherNumberingSystems")) { 582 handleNumberingSystem(value, examples); 583 } else if (parts.contains("currencyFormats") && parts.contains("unitPattern")) { 584 formatCountValue(xpath, parts, value, examples); 585 } else if (parts.getElement(-1).equals("compoundUnitPattern")) { 586 handleCompoundUnit(parts, examples); 587 } else if (parts.getElement(-1).equals("compoundUnitPattern1") 588 || parts.getElement(-1).equals("unitPrefixPattern")) { 589 handleCompoundUnit1(parts, value, examples); 590 } else if (parts.getElement(-2).equals("unit") 591 && (parts.getElement(-1).equals("unitPattern") 592 || parts.getElement(-1).equals("displayName"))) { 593 handleFormatUnit(parts, value, examples); 594 } else if (parts.getElement(-1).equals("perUnitPattern")) { 595 handleFormatPerUnit(value, examples); 596 } else if (parts.getElement(-2).equals("minimalPairs")) { 597 handleMinimalPairs(parts, value, examples); 598 } else if (parts.getElement(-1).equals("durationUnitPattern")) { 599 handleDurationUnit(value, examples); 600 } else if (parts.contains("intervalFormats")) { 601 handleIntervalFormats(parts, value, examples); 602 } else if (parts.getElement(1).equals("delimiters")) { 603 handleDelimiters(parts, xpath, value, examples); 604 } else if (parts.getElement(1).equals("listPatterns")) { 605 handleListPatterns(parts, value, examples); 606 } else if (parts.getElement(2).equals("ellipsis")) { 607 handleEllipsis(parts.getAttributeValue(-1, "type"), value, examples); 608 } else if (parts.getElement(-1).equals("monthPattern")) { 609 handleMonthPatterns(parts, value, examples); 610 } else if (parts.getElement(-1).equals("appendItem")) { 611 handleAppendItems(parts, value, examples); 612 } else if (parts.getElement(-1).equals("annotation")) { 613 handleAnnotationName(parts, value, examples); 614 } else if (parts.getElement(-1).equals("characterLabel")) { 615 handleLabel(parts, value, examples); 616 } else if (parts.getElement(-1).equals("characterLabelPattern")) { 617 handleLabelPattern(parts, value, examples); 618 } else if (parts.getElement(1).equals("personNames")) { 619 handlePersonName(parts, value, examples); 620 } else if (parts.getElement(-1).equals("exemplarCharacters") 621 || parts.getElement(-1).equals("parseLenient")) { 622 handleUnicodeSet(parts, xpath, value, examples); 623 } 624 } 625 626 // Note: may want to change to locale's order; if so, these would be instance fields 627 static final SimpleUnicodeSetFormatter SUSF = 628 new SimpleUnicodeSetFormatter(SimpleUnicodeSetFormatter.BASIC_COLLATOR); 629 static final SimpleUnicodeSetFormatter SUSFNS = 630 new SimpleUnicodeSetFormatter( 631 SimpleUnicodeSetFormatter.BASIC_COLLATOR, 632 CodePointEscaper.FORCE_ESCAPE_WITH_NONSPACING); 633 static final String LRM = "\u200E"; 634 static final UnicodeSet NEEDS_LRM = new UnicodeSet("[:BidiClass=R:]").freeze(); 635 private static final boolean SHOW_NON_SPACING_IN_UNICODE_SET = true; 636 637 /** 638 * Add examples for UnicodeSets. First, show a hex format of non-spacing marks if there are any, 639 * then show delta to the winning value if there are any. 640 */ handleUnicodeSet( XPathParts parts, String xpath, String value, List<String> examples)641 private void handleUnicodeSet( 642 XPathParts parts, String xpath, String value, List<String> examples) { 643 UnicodeSet valueSet; 644 try { 645 valueSet = new UnicodeSet(value); 646 } catch (Exception e) { 647 return; 648 } 649 String winningValue = cldrFile.getWinningValue(xpath); 650 if (!winningValue.equals(value)) { 651 // show delta 652 final UnicodeSet winningSet = new UnicodeSet(winningValue); 653 UnicodeSet value_minus_winning = new UnicodeSet(valueSet).removeAll(winningSet); 654 UnicodeSet winning_minus_value = new UnicodeSet(winningSet).removeAll(valueSet); 655 if (!value_minus_winning.isEmpty()) { 656 examples.add(LRM + ADDS + " " + SUSF.format(value_minus_winning)); 657 } 658 if (!winning_minus_value.isEmpty()) { 659 examples.add(LRM + SUBTRACTS + " " + SUSF.format(winning_minus_value)); 660 } 661 } 662 if (parts.containsAttributeValue("type", "auxiliary")) { 663 LanguageTagParser ltp = new LanguageTagParser(); 664 String ltpScript = ltp.set(cldrFile.getLocaleID()).getResolvedScript(); 665 UnicodeSet exemplars = ScriptToExemplars.getExemplars(ltpScript); 666 UnicodeSet main = cldrFile.getExemplarSet(ExemplarType.main, WinningChoice.WINNING); 667 UnicodeSet mainAndAux = new UnicodeSet(main).addAll(valueSet); 668 if (!mainAndAux.containsAll(exemplars)) { 669 examples.add( 670 LRM 671 + HINTS 672 + " " 673 + SUSF.format(new UnicodeSet(exemplars).removeAll(mainAndAux))); 674 } 675 } 676 if (SHOW_NON_SPACING_IN_UNICODE_SET 677 && valueSet.containsSome(CodePointEscaper.FORCE_ESCAPE)) { 678 for (String nsm : new UnicodeSet(valueSet).retainAll(CodePointEscaper.FORCE_ESCAPE)) { 679 examples.add(CodePointEscaper.toExample(nsm.codePointAt(0))); 680 } 681 } 682 examples.add(setBackground(INTERNAL) + valueSet.toPattern(false)); // internal format 683 } 684 685 /** 686 * Holds a map and an object that are relatively expensive to build, so we don't want to do that 687 * on each call. TODO clean up the synchronization model. 688 */ 689 private static class PersonNamesCache implements ExampleCache.ClearableCache { 690 Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNames = null; 691 PersonNameFormatter personNameFormatter = null; 692 693 @Override clear()694 public void clear() { 695 sampleNames = null; 696 personNameFormatter = null; 697 } 698 getSampleNames(CLDRFile cldrFile)699 Map<PersonNameFormatter.SampleType, SimpleNameObject> getSampleNames(CLDRFile cldrFile) { 700 synchronized (this) { 701 if (sampleNames == null) { 702 sampleNames = PersonNameFormatter.loadSampleNames(cldrFile); 703 } 704 return sampleNames; 705 } 706 } 707 getPersonNameFormatter(CLDRFile cldrFile)708 PersonNameFormatter getPersonNameFormatter(CLDRFile cldrFile) { 709 synchronized (this) { 710 if (personNameFormatter == null) { 711 personNameFormatter = new PersonNameFormatter(cldrFile); 712 } 713 return personNameFormatter; 714 } 715 } 716 717 @Override toString()718 public String toString() { 719 return "[" 720 + (sampleNames == null ? "" : Joiner.on('\n').join(sampleNames.entrySet())) 721 + ", " 722 + (personNameFormatter == null ? "" : personNameFormatter.toString()) 723 + "]"; 724 } 725 } 726 727 /** Register the cache, so that it gets cleared when any of the paths change */ 728 PersonNamesCache personNamesCache = 729 exCache.registerCache( 730 new PersonNamesCache(), 731 "//ldml/personNames/sampleName[@item=\"*\"]/nameField[@type=\"*\"]", 732 "//ldml/personNames/initialPattern[@type=\"*\"]", 733 "//ldml/personNames/foreignSpaceReplacement", 734 "//ldml/personNames/nativeSpaceReplacement", 735 "//ldml/personNames/personName[@order=\"*\"][@length=\"*\"][@usage=\"*\"][@formality=\"*\"]/namePattern"); 736 737 private static final Function<String, String> BACKGROUND_TRANSFORM = 738 x -> backgroundStartSymbol + x + backgroundEndSymbol; 739 handlePersonName(XPathParts parts, String value, List<String> examples)740 private void handlePersonName(XPathParts parts, String value, List<String> examples) { 741 // ldml/personNames/personName[@order="givenFirst"][@length="long"][@usage="addressing"][@style="formal"]/namePattern => {prefix} {surname} 742 String debugState = "start"; 743 try { 744 FormatParameters formatParameters = 745 new FormatParameters( 746 PersonNameFormatter.Order.from(parts.getAttributeValue(2, "order")), 747 PersonNameFormatter.Length.from(parts.getAttributeValue(2, "length")), 748 PersonNameFormatter.Usage.from(parts.getAttributeValue(2, "usage")), 749 PersonNameFormatter.Formality.from( 750 parts.getAttributeValue(2, "formality"))); 751 final CLDRFile cldrFile2 = getCldrFile(); 752 switch (parts.getElement(2)) { 753 case "nameOrderLocales": 754 for (String localeId : PersonNameFormatter.SPLIT_SPACE.split(value)) { 755 final String name = 756 localeId.equals("und") 757 ? "«any other»" 758 : cldrFile2.getName(localeId); 759 examples.add(localeId + " = " + name); 760 } 761 break; 762 case "personName": 763 Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNames = 764 personNamesCache.getSampleNames(cldrFile2); 765 PersonNameFormatter personNameFormatter = 766 personNamesCache.getPersonNameFormatter(cldrFile2); 767 768 // We might need the alt, however: String alt = parts.getAttributeValue(-1, 769 // "alt"); 770 771 boolean lastIsNative = false; 772 for (Entry<PersonNameFormatter.SampleType, SimpleNameObject> 773 typeAndSampleNameObject : sampleNames.entrySet()) { 774 NamePattern namePattern = NamePattern.from(0, value); // get the first one 775 final boolean isNative = typeAndSampleNameObject.getKey().isNative(); 776 if (isNative != lastIsNative) { 777 final String title = 778 isNative 779 ? " Native name and script:" 780 : " Foreign name and native script:"; 781 examples.add(startItalicSymbol + title + endItalicSymbol); 782 lastIsNative = isNative; 783 } 784 debugState = "<NamePattern.from: " + namePattern; 785 final FallbackFormatter fallbackInfo = 786 personNameFormatter.getFallbackInfo(); 787 debugState = "<getFallbackInfo: " + fallbackInfo; 788 final NameObject nameObject = 789 new PersonNameFormatter.TransformingNameObject( 790 typeAndSampleNameObject.getValue(), BACKGROUND_TRANSFORM); 791 String result = 792 namePattern.format(nameObject, formatParameters, fallbackInfo); 793 debugState = "<namePattern.format: " + result; 794 examples.add(result); 795 } 796 // Extra names 797 final String script = 798 new LikelySubtags().getLikelyScript(cldrFile.getLocaleID()); 799 Output<Boolean> haveHeaderLine = new Output<>(false); 800 801 if (!script.equals("Latn")) { 802 formatSampleName(formatParameters, englishFile, examples, haveHeaderLine); 803 } 804 if (!script.equals("Cyrl")) { 805 formatSampleName( 806 formatParameters, PersonNameScripts.Cyrl, examples, haveHeaderLine); 807 } 808 if (!script.equals("Jpan")) { 809 formatSampleName( 810 formatParameters, PersonNameScripts.Jpan, examples, haveHeaderLine); 811 } 812 break; 813 } 814 } catch (Exception e) { 815 StringBuffer stackTrace; 816 try (StringWriter sw = new StringWriter(); 817 PrintWriter p = new PrintWriter(sw)) { 818 e.printStackTrace(p); 819 stackTrace = sw.getBuffer(); 820 } catch (Exception e2) { 821 stackTrace = new StringBuffer("internal error"); 822 } 823 examples.add( 824 "Internal error: " + e.getMessage() + "\n" + debugState + "\n" + stackTrace); 825 } 826 } 827 828 enum PersonNameScripts { 829 Latn, 830 Cyrl, 831 Jpan 832 } 833 formatSampleName( FormatParameters formatParameters, PersonNameScripts script, List<String> examples, Output<Boolean> haveHeaderLine)834 public void formatSampleName( 835 FormatParameters formatParameters, 836 PersonNameScripts script, 837 List<String> examples, 838 Output<Boolean> haveHeaderLine) { 839 switch (script) { 840 case Cyrl: 841 if (cyrillicFile == null) { 842 cyrillicFile = CLDRConfig.getInstance().getCldrFactory().make("uk", true); 843 } 844 formatSampleName(formatParameters, cyrillicFile, examples, haveHeaderLine); 845 break; 846 case Jpan: 847 if (japanFile == null) { 848 japanFile = CLDRConfig.getInstance().getCldrFactory().make("ja", true); 849 } 850 formatSampleName(formatParameters, japanFile, examples, haveHeaderLine); 851 break; 852 default: 853 throw new IllegalArgumentException(); 854 } 855 } 856 formatSampleName( FormatParameters formatParameters, final CLDRFile cldrFile2, List<String> examples, Output<Boolean> haveHeaderLine)857 public void formatSampleName( 858 FormatParameters formatParameters, 859 final CLDRFile cldrFile2, 860 List<String> examples, 861 Output<Boolean> haveHeaderLine) { 862 PersonNameFormatter formatter2 = new PersonNameFormatter(cldrFile2); 863 Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNames2 = 864 PersonNameFormatter.loadSampleNames(cldrFile2); 865 SimpleNameObject sampleName = 866 getBestAvailable( 867 sampleNames2, 868 PersonNameFormatter.SampleType.nativeFull, 869 PersonNameFormatter.SampleType.nativeGGS); 870 if (sampleName != null) { 871 String result2 = 872 formatter2.format( 873 new PersonNameFormatter.TransformingNameObject( 874 sampleName, BACKGROUND_TRANSFORM), 875 formatParameters); 876 if (result2 != null) { 877 if (!haveHeaderLine.value) { 878 haveHeaderLine.value = Boolean.TRUE; 879 examples.add( 880 startItalicSymbol + " Foreign name and script:" + endItalicSymbol); 881 } 882 examples.add(result2); 883 } 884 } 885 } 886 getBestAvailable( Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNamesMap, PersonNameFormatter.SampleType... sampleTypes)887 private SimpleNameObject getBestAvailable( 888 Map<PersonNameFormatter.SampleType, SimpleNameObject> sampleNamesMap, 889 PersonNameFormatter.SampleType... sampleTypes) { 890 for (PersonNameFormatter.SampleType sampleType : sampleTypes) { 891 SimpleNameObject result = sampleNamesMap.get(sampleType); 892 if (result != null) { 893 return result; 894 } 895 } 896 return null; 897 } 898 handleLabelPattern(XPathParts parts, String value, List<String> examples)899 private void handleLabelPattern(XPathParts parts, String value, List<String> examples) { 900 if ("category-list".equals(parts.getAttributeValue(-1, "type"))) { 901 CLDRFile cfile = getCldrFile(); 902 SimpleFormatter initialPattern = SimpleFormatter.compile(setBackground(value)); 903 String path = CLDRFile.getKey(CLDRFile.TERRITORY_NAME, "FR"); 904 String regionName = cfile.getStringValue(path); 905 String flagName = 906 cfile.getStringValue("//ldml/characterLabels/characterLabel[@type=\"flag\"]"); 907 examples.add( 908 invertBackground( 909 EmojiConstants.getEmojiFromRegionCodes("FR") 910 + " ⇒ " 911 + initialPattern.format(flagName, regionName))); 912 } 913 } 914 handleLabel(XPathParts parts, String value, List<String> examples)915 private void handleLabel(XPathParts parts, String value, List<String> examples) { 916 // "//ldml/characterLabels/characterLabel[@type=\"" + typeAttributeValue + "\"]" 917 switch (parts.getAttributeValue(-1, "type")) { 918 case "flag": 919 { 920 String value2 = backgroundStartSymbol + value + backgroundEndSymbol; 921 CLDRFile cfile = getCldrFile(); 922 SimpleFormatter initialPattern = 923 SimpleFormatter.compile( 924 cfile.getStringValue( 925 "//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 926 addFlag(value2, "FR", cfile, initialPattern, examples); 927 addFlag(value2, "CN", cfile, initialPattern, examples); 928 addSubdivisionFlag(value2, "gbeng", initialPattern, examples); 929 addSubdivisionFlag(value2, "gbsct", initialPattern, examples); 930 addSubdivisionFlag(value2, "gbwls", initialPattern, examples); 931 return; 932 } 933 case "keycap": 934 { 935 String value2 = backgroundStartSymbol + value + backgroundEndSymbol; 936 CLDRFile cfile = getCldrFile(); 937 SimpleFormatter initialPattern = 938 SimpleFormatter.compile( 939 cfile.getStringValue( 940 "//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 941 examples.add(invertBackground(initialPattern.format(value2, "1"))); 942 examples.add(invertBackground(initialPattern.format(value2, "10"))); 943 examples.add(invertBackground(initialPattern.format(value2, "#"))); 944 return; 945 } 946 default: 947 return; 948 } 949 } 950 addFlag( String value2, String isoRegionCode, CLDRFile cfile, SimpleFormatter initialPattern, List<String> examples)951 private void addFlag( 952 String value2, 953 String isoRegionCode, 954 CLDRFile cfile, 955 SimpleFormatter initialPattern, 956 List<String> examples) { 957 String path = CLDRFile.getKey(CLDRFile.TERRITORY_NAME, isoRegionCode); 958 String regionName = cfile.getStringValue(path); 959 examples.add( 960 invertBackground( 961 EmojiConstants.getEmojiFromRegionCodes(isoRegionCode) 962 + " ⇒ " 963 + initialPattern.format(value2, regionName))); 964 } 965 addSubdivisionFlag( String value2, String isoSubdivisionCode, SimpleFormatter initialPattern, List<String> examples)966 private void addSubdivisionFlag( 967 String value2, 968 String isoSubdivisionCode, 969 SimpleFormatter initialPattern, 970 List<String> examples) { 971 String subdivisionName = subdivisionIdToName.get(isoSubdivisionCode); 972 if (subdivisionName == null) { 973 subdivisionName = isoSubdivisionCode; 974 } 975 examples.add( 976 invertBackground( 977 EmojiConstants.getEmojiFromSubdivisionCodes(isoSubdivisionCode) 978 + " ⇒ " 979 + initialPattern.format(value2, subdivisionName))); 980 } 981 handleAnnotationName(XPathParts parts, String value, List<String> examples)982 private void handleAnnotationName(XPathParts parts, String value, List<String> examples) { 983 // ldml/annotations/annotation[@cp=""][@type="tts"] 984 // skip anything but the name 985 if (!"tts".equals(parts.getAttributeValue(-1, "type"))) { 986 return; 987 } 988 String cp = parts.getAttributeValue(-1, "cp"); 989 if (cp == null || cp.isEmpty()) { 990 return; 991 } 992 int first = cp.codePointAt(0); 993 switch (first) { 994 case 0x1F46A: // U+1F46A FAMILY 995 examples.add(formatGroup(value, "", "", "", "", "")); 996 examples.add(formatGroup(value, "", "", "", "")); 997 break; 998 case 0x1F48F: // U+1F48F KISS 999 examples.add(formatGroup(value, "❤️", "", "")); 1000 examples.add(formatGroup(value, "❤️", "", "")); 1001 break; 1002 case 0x1F491: // U+1F491 COUPLE WITH HEART 1003 examples.add(formatGroup(value, "❤️", "", "")); 1004 examples.add(formatGroup(value, "❤️", "", "")); 1005 break; 1006 default: 1007 boolean isSkin = EmojiConstants.MODIFIERS.contains(first); 1008 if (isSkin || EmojiConstants.HAIR.contains(first)) { 1009 String value2 = backgroundStartSymbol + value + backgroundEndSymbol; 1010 CLDRFile cfile = getCldrFile(); 1011 String skin = ""; 1012 String hair = ""; 1013 String skinName = getEmojiName(cfile, skin); 1014 String hairName = getEmojiName(cfile, hair); 1015 if (hairName == null) { 1016 hair = "[missing]"; 1017 } 1018 SimpleFormatter initialPattern = 1019 SimpleFormatter.compile( 1020 cfile.getStringValue( 1021 "//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 1022 SimpleFormatter listPattern = 1023 SimpleFormatter.compile( 1024 cfile.getStringValue( 1025 "//ldml/listPatterns/listPattern[@type=\"unit-short\"]/listPatternPart[@type=\"2\"]")); 1026 1027 hair = EmojiConstants.JOINER_STRING + hair; 1028 formatPeople( 1029 cfile, 1030 first, 1031 isSkin, 1032 value2, 1033 "", 1034 skin, 1035 skinName, 1036 hair, 1037 hairName, 1038 initialPattern, 1039 listPattern, 1040 examples); 1041 formatPeople( 1042 cfile, 1043 first, 1044 isSkin, 1045 value2, 1046 "", 1047 skin, 1048 skinName, 1049 hair, 1050 hairName, 1051 initialPattern, 1052 listPattern, 1053 examples); 1054 } 1055 break; 1056 } 1057 } 1058 getEmojiName(CLDRFile cfile, String skin)1059 private String getEmojiName(CLDRFile cfile, String skin) { 1060 return cfile.getStringValue( 1061 "//ldml/annotations/annotation[@cp=\"" + skin + "\"][@type=\"tts\"]"); 1062 } 1063 1064 // ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"] formatGroup(String value, String sourceEmoji, String... components)1065 private String formatGroup(String value, String sourceEmoji, String... components) { 1066 CLDRFile cfile = getCldrFile(); 1067 SimpleFormatter initialPattern = 1068 SimpleFormatter.compile( 1069 cfile.getStringValue( 1070 "//ldml/characterLabels/characterLabelPattern[@type=\"category-list\"]")); 1071 String value2 = backgroundEndSymbol + value + backgroundStartSymbol; 1072 String[] names = new String[components.length]; 1073 int i = 0; 1074 for (String component : components) { 1075 names[i++] = getEmojiName(cfile, component); 1076 } 1077 return backgroundStartSymbol 1078 + sourceEmoji 1079 + " ⇒ " 1080 + initialPattern.format( 1081 value2, 1082 longListPatternExample( 1083 EmojiConstants.COMPOSED_NAME_LIST.getPath(), "n/a", "n/a2", names)); 1084 } 1085 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)1086 private void formatPeople( 1087 CLDRFile cfile, 1088 int first, 1089 boolean isSkin, 1090 String value2, 1091 String person, 1092 String skin, 1093 String skinName, 1094 String hair, 1095 String hairName, 1096 SimpleFormatter initialPattern, 1097 SimpleFormatter listPattern, 1098 Collection<String> examples) { 1099 String cp; 1100 String personName = getEmojiName(cfile, person); 1101 StringBuilder emoji = new StringBuilder(person).appendCodePoint(first); 1102 cp = UTF16.valueOf(first); 1103 cp = isSkin ? cp : EmojiConstants.JOINER_STRING + cp; 1104 examples.add( 1105 person + cp + " ⇒ " + invertBackground(initialPattern.format(personName, value2))); 1106 emoji.setLength(0); 1107 emoji.append(personName); 1108 if (isSkin) { 1109 skinName = value2; 1110 skin = cp; 1111 } else { 1112 hairName = value2; 1113 hair = cp; 1114 } 1115 examples.add( 1116 person 1117 + skin 1118 + hair 1119 + " ⇒ " 1120 + invertBackground( 1121 listPattern.format( 1122 initialPattern.format(personName, skinName), hairName))); 1123 } 1124 handleDayPeriod(XPathParts parts, String value, List<String> examples)1125 private void handleDayPeriod(XPathParts parts, String value, List<String> examples) { 1126 // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="morning1"] 1127 // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="stand-alone"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="morning1"] 1128 final String dayPeriodType = parts.getAttributeValue(5, "type"); 1129 if (dayPeriodType == null) { 1130 return; // formerly happened for some "/alias" paths 1131 } 1132 org.unicode.cldr.util.DayPeriodInfo.Type aType = 1133 dayPeriodType.equals("format") 1134 ? DayPeriodInfo.Type.format 1135 : DayPeriodInfo.Type.selection; 1136 DayPeriodInfo dayPeriodInfo = 1137 supplementalDataInfo.getDayPeriods(aType, cldrFile.getLocaleID()); 1138 String periodString = parts.getAttributeValue(-1, "type"); 1139 if (periodString == null) { 1140 return; // formerly happened for some "/alias" paths 1141 } 1142 DayPeriod dayPeriod = DayPeriod.valueOf(periodString); 1143 String periods = dayPeriodInfo.toString(dayPeriod); 1144 examples.add(periods); 1145 if ("format".equals(dayPeriodType)) { 1146 if (value == null) { 1147 value = "�"; 1148 } 1149 R3<Integer, Integer, Boolean> info = dayPeriodInfo.getFirstDayPeriodInfo(dayPeriod); 1150 if (info != null) { 1151 int time = (((info.get0() + info.get1()) % DayPeriodInfo.DAY_LIMIT) / 2); 1152 String timeFormatString = 1153 icuServiceBuilder.formatDayPeriod( 1154 time, backgroundStartSymbol + value + backgroundEndSymbol); 1155 examples.add(invertBackground(timeFormatString)); 1156 } 1157 } 1158 } 1159 handleDateSymbol(XPathParts parts, String value, List<String> examples)1160 private void handleDateSymbol(XPathParts parts, String value, List<String> examples) { 1161 // Currently only called for month names, can expand in the future to handle other symbols. 1162 // The idea is to show format months in a yMMMM?d date format, and stand-alone months in a 1163 // yMMMM? format. 1164 String length = parts.findAttributeValue("monthWidth", "type"); // wide, abbreviated, narrow 1165 if (length.equals("narrow")) { 1166 return; // no examples for narrow 1167 } 1168 String context = parts.findAttributeValue("monthContext", "type"); // format, stand-alone 1169 String calendarId = 1170 parts.findAttributeValue("calendar", "type"); // gregorian, islamic, hebrew, ... 1171 String monthNumId = 1172 parts.findAttributeValue("month", "type"); // 1-based: 1, 2, 3, ... 12 or 13 1173 1174 final String[] skeletons = {"yMMMMd", "yMMMd", "yMMMM", "yMMM"}; 1175 int skeletonIndex = (length.equals("wide")) ? 0 : 1; 1176 if (!context.equals("format")) { 1177 skeletonIndex += 2; 1178 } 1179 String checkPath = 1180 "//ldml/dates/calendars/calendar[@type=\"" 1181 + calendarId 1182 + "\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" 1183 + skeletons[skeletonIndex] 1184 + "\"]"; 1185 String dateFormat = cldrFile.getWinningValue(checkPath); 1186 if (dateFormat == null || dateFormat.indexOf("MMM") < 0) { 1187 // If we do not have the desired width (might be missing for MMMM) or 1188 // the desired format does not have alpha months (in some locales liks 'cs' 1189 // skeletons for MMM have pattern with M), then try the other width for same 1190 // context by adjusting skeletonIndex. 1191 skeletonIndex = (length.equals("wide")) ? skeletonIndex + 1 : skeletonIndex - 1; 1192 checkPath = 1193 "//ldml/dates/calendars/calendar[@type=\"" 1194 + calendarId 1195 + "\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" 1196 + skeletons[skeletonIndex] 1197 + "\"]"; 1198 dateFormat = cldrFile.getWinningValue(checkPath); 1199 } 1200 if (dateFormat == null) { 1201 return; 1202 } 1203 SimpleDateFormat sdf = icuServiceBuilder.getDateFormat(calendarId, dateFormat); 1204 sdf.setTimeZone(ZONE_SAMPLE); 1205 DateFormatSymbols dfs = sdf.getDateFormatSymbols(); 1206 // We do not know whether dateFormat is using MMMM, MMM, LLLL or LLL so 1207 // override all of them in our DateFormatSymbols. The DATE_SAMPLE is for 1208 // month 9 "September", offset of 8 in the months arrays, so override that. 1209 String[] monthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE); 1210 monthNames[8] = value; 1211 dfs.setMonths(monthNames, DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE); 1212 monthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED); 1213 monthNames[8] = value; 1214 dfs.setMonths(monthNames, DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED); 1215 monthNames = dfs.getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE); 1216 monthNames[8] = value; 1217 dfs.setMonths(monthNames, DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE); 1218 monthNames = dfs.getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED); 1219 monthNames[8] = value; 1220 dfs.setMonths(monthNames, DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED); 1221 sdf.setDateFormatSymbols(dfs); 1222 examples.add(sdf.format(DATE_SAMPLE)); 1223 } 1224 handleMinimalPairs( XPathParts parts, String minimalPattern, List<String> examples)1225 private void handleMinimalPairs( 1226 XPathParts parts, String minimalPattern, List<String> examples) { 1227 Output<String> output = new Output<>(); 1228 String count; 1229 String otherCount; 1230 String sample; 1231 String sampleBad; 1232 String locale = getCldrFile().getLocaleID(); 1233 1234 switch (parts.getElement(-1)) { 1235 case "ordinalMinimalPairs": // ldml/numbers/minimalPairs/ordinalMinimalPairs[@count="one"] 1236 count = parts.getAttributeValue(-1, "ordinal"); 1237 sample = 1238 bestMinimalPairSamples.getPluralOrOrdinalSample( 1239 PluralType.ordinal, 1240 count); // Pick a unit that exhibits the most variation 1241 otherCount = getOtherCount(locale, PluralType.ordinal, count); 1242 sampleBad = 1243 bestMinimalPairSamples.getPluralOrOrdinalSample( 1244 PluralType.ordinal, 1245 otherCount); // Pick a unit that exhibits the most variation 1246 break; 1247 1248 case "pluralMinimalPairs": // ldml/numbers/minimalPairs/pluralMinimalPairs[@count="one"] 1249 count = parts.getAttributeValue(-1, "count"); 1250 sample = 1251 bestMinimalPairSamples.getPluralOrOrdinalSample( 1252 PluralType.cardinal, 1253 count); // Pick a unit that exhibits the most variation 1254 otherCount = getOtherCount(locale, PluralType.cardinal, count); 1255 sampleBad = 1256 bestMinimalPairSamples.getPluralOrOrdinalSample( 1257 PluralType.cardinal, 1258 otherCount); // Pick a unit that exhibits the most variation 1259 break; 1260 1261 case "caseMinimalPairs": // ldml/numbers/minimalPairs/caseMinimalPairs[@case="accusative"] 1262 String gCase = parts.getAttributeValue(-1, "case"); 1263 sample = 1264 bestMinimalPairSamples.getBestUnitWithCase( 1265 gCase, output); // Pick a unit that exhibits the most variation 1266 sampleBad = getOtherCase(sample); 1267 break; 1268 1269 case "genderMinimalPairs": // ldml/numbers/minimalPairs/genderMinimalPairs[@gender="feminine"] 1270 String gender = parts.getAttributeValue(-1, "gender"); 1271 sample = bestMinimalPairSamples.getBestUnitWithGender(gender, output); 1272 String otherGender = getOtherGender(gender); 1273 sampleBad = bestMinimalPairSamples.getBestUnitWithGender(otherGender, output); 1274 break; 1275 default: 1276 return; 1277 } 1278 String formattedUnit = 1279 format(minimalPattern, backgroundStartSymbol + sample + backgroundEndSymbol); 1280 examples.add(formattedUnit); 1281 if (sampleBad == null) { 1282 sampleBad = "n/a"; 1283 } 1284 formattedUnit = 1285 format(minimalPattern, backgroundStartSymbol + sampleBad + backgroundEndSymbol); 1286 examples.add(EXAMPLE_OF_INCORRECT + formattedUnit); 1287 } 1288 getOtherGender(String gender)1289 private String getOtherGender(String gender) { 1290 if (gender == null || grammarInfo == null) { 1291 return null; 1292 } 1293 Collection<String> unitGenders = 1294 grammarInfo.get( 1295 GrammaticalTarget.nominal, 1296 GrammaticalFeature.grammaticalGender, 1297 GrammaticalScope.units); 1298 for (String otherGender : unitGenders) { 1299 if (!gender.equals(otherGender)) { 1300 return otherGender; 1301 } 1302 } 1303 return null; 1304 } 1305 getOtherCase(String sample)1306 private String getOtherCase(String sample) { 1307 if (sample == null) { 1308 return null; 1309 } 1310 Collection<String> unitCases = 1311 grammarInfo.get( 1312 GrammaticalTarget.nominal, 1313 GrammaticalFeature.grammaticalCase, 1314 GrammaticalScope.units); 1315 Output<String> output = new Output<>(); 1316 for (String otherCase : unitCases) { 1317 String sampleBad = 1318 bestMinimalPairSamples.getBestUnitWithCase( 1319 otherCase, output); // Pick a unit that exhibits the most variation 1320 if (!sample.equals(sampleBad)) { // caution: sampleBad may be null 1321 return sampleBad; 1322 } 1323 } 1324 return null; 1325 } 1326 getOtherCount(String locale, PluralType ordinal, String count)1327 private static String getOtherCount(String locale, PluralType ordinal, String count) { 1328 String otherCount = null; 1329 if (!Objects.equals(count, "other")) { 1330 otherCount = "other"; 1331 } else { 1332 PluralInfo rules = SupplementalDataInfo.getInstance().getPlurals(ordinal, locale); 1333 Set<String> counts = rules.getAdjustedCountStrings(); 1334 for (String tryCount : counts) { 1335 if (!tryCount.equals("other")) { 1336 otherCount = tryCount; 1337 break; 1338 } 1339 } 1340 } 1341 return otherCount; 1342 } 1343 getUnitLength(XPathParts parts)1344 private UnitLength getUnitLength(XPathParts parts) { 1345 return UnitLength.valueOf(parts.getAttributeValue(-3, "type").toUpperCase(Locale.ENGLISH)); 1346 } 1347 handleFormatUnit(XPathParts parts, String unitPattern, List<String> examples)1348 private void handleFormatUnit(XPathParts parts, String unitPattern, List<String> examples) { 1349 // Sample: 1350 // //ldml/units/unitLength[@type="long"]/unit[@type="duration-day"]/unitPattern[@count="one"][@case="accusative"] 1351 1352 String longUnitId = parts.getAttributeValue(-2, "type"); 1353 final String shortUnitId = UNIT_CONVERTER.getShortId(longUnitId); 1354 if (UnitConverter.HACK_SKIP_UNIT_NAMES.contains(shortUnitId)) { 1355 return; 1356 } 1357 1358 if (parts.getElement(-1).equals("unitPattern")) { 1359 String count = parts.getAttributeValue(-1, "count"); 1360 DecimalQuantity amount = getBest(Count.valueOf(count)); 1361 if (amount != null) { 1362 addFormattedUnits(examples, parts, unitPattern, shortUnitId, amount); 1363 } 1364 } 1365 // add related units 1366 Map<Rational, String> relatedUnits = 1367 UNIT_CONVERTER.getRelatedExamples( 1368 shortUnitId, UnitConverter.getExampleUnitSystems(cldrFile.getLocaleID())); 1369 String unitSystem = null; 1370 boolean first = true; 1371 for (Entry<Rational, String> relatedUnitInfo : relatedUnits.entrySet()) { 1372 if (unitSystem == null) { 1373 Set<UnitSystem> systems = UNIT_CONVERTER.getSystemsEnum(shortUnitId); 1374 unitSystem = UnitSystem.getSystemsDisplay(systems); 1375 } 1376 Rational relatedValue = relatedUnitInfo.getKey(); 1377 String relatedUnit = relatedUnitInfo.getValue(); 1378 Set<UnitSystem> systems = UNIT_CONVERTER.getSystemsEnum(relatedUnit); 1379 String relation = "≡"; 1380 String relatedValueDisplay = relatedValue.toString(FormatStyle.approx); 1381 if (relatedValueDisplay.startsWith("~")) { 1382 relation = "≈"; 1383 relatedValueDisplay = relatedValueDisplay.substring(1); 1384 } 1385 if (first) { 1386 if (!examples.isEmpty()) { 1387 examples.add(""); // add blank line 1388 } 1389 first = false; 1390 } 1391 examples.add( 1392 String.format( 1393 "1 %s%s %s %s %s%s", 1394 shortUnitId, 1395 unitSystem, 1396 relation, 1397 relatedValueDisplay, 1398 relatedUnit, 1399 UnitSystem.getSystemsDisplay(systems))); 1400 } 1401 } 1402 1403 /** 1404 * Handles paths like:<br> 1405 * //ldml/units/unitLength[@type="long"]/unit[@type="volume-fluid-ounce-imperial"]/unitPattern[@count="other"] 1406 */ addFormattedUnits( List<String> examples, XPathParts parts, String unitPattern, final String shortUnitId, DecimalQuantity amount)1407 public void addFormattedUnits( 1408 List<String> examples, 1409 XPathParts parts, 1410 String unitPattern, 1411 final String shortUnitId, 1412 DecimalQuantity amount) { 1413 /* 1414 * PluralRules.FixedDecimal is deprecated, but deprecated in ICU is 1415 * also used to mark internal methods (which are OK for us to use in CLDR). 1416 */ 1417 DecimalFormat numberFormat; 1418 String formattedAmount; 1419 numberFormat = icuServiceBuilder.getNumberFormat(1); 1420 formattedAmount = numberFormat.format(amount.toBigDecimal()); 1421 examples.add( 1422 format(unitPattern, backgroundStartSymbol + formattedAmount + backgroundEndSymbol)); 1423 1424 if (parts.getElement(-2).equals("unit")) { 1425 if (unitPattern != null) { 1426 String gCase = parts.getAttributeValue(-1, "case"); 1427 if (gCase == null) { 1428 gCase = GrammaticalFeature.grammaticalCase.getDefault(null); 1429 } 1430 Collection<String> unitCaseInfo = null; 1431 if (grammarInfo != null) { 1432 unitCaseInfo = 1433 grammarInfo.get( 1434 GrammaticalTarget.nominal, 1435 GrammaticalFeature.grammaticalCase, 1436 GrammaticalScope.units); 1437 } 1438 String minimalPattern = 1439 cldrFile.getStringValue( 1440 "//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\"" 1441 + gCase 1442 + "\"]"); 1443 if (minimalPattern != null) { 1444 String composed = 1445 format( 1446 unitPattern, 1447 backgroundStartSymbol + formattedAmount + backgroundEndSymbol); 1448 examples.add( 1449 backgroundStartSymbol 1450 + format( 1451 minimalPattern, 1452 backgroundEndSymbol + composed + backgroundStartSymbol) 1453 + backgroundEndSymbol); 1454 // get contrasting case 1455 if (unitCaseInfo != null && !unitCaseInfo.isEmpty()) { 1456 String constrastingCase = 1457 getConstrastingCase(unitPattern, gCase, unitCaseInfo, parts); 1458 if (constrastingCase != null) { 1459 minimalPattern = 1460 cldrFile.getStringValue( 1461 "//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\"" 1462 + constrastingCase 1463 + "\"]"); 1464 composed = 1465 format( 1466 unitPattern, 1467 backgroundStartSymbol 1468 + formattedAmount 1469 + backgroundEndSymbol); 1470 examples.add( 1471 EXAMPLE_OF_INCORRECT 1472 + backgroundStartSymbol 1473 + format( 1474 minimalPattern, 1475 backgroundEndSymbol 1476 + composed 1477 + backgroundStartSymbol) 1478 + backgroundEndSymbol); 1479 } 1480 } else { 1481 examples.add(EXAMPLE_OF_CAUTION + "️No Case Minimal Pair available yet️"); 1482 } 1483 } 1484 } 1485 } 1486 } 1487 getConstrastingCase( String unitPattern, String gCase, Collection<String> unitCaseInfo, XPathParts parts)1488 private String getConstrastingCase( 1489 String unitPattern, String gCase, Collection<String> unitCaseInfo, XPathParts parts) { 1490 for (String otherCase : unitCaseInfo) { 1491 if (otherCase.equals(gCase)) { 1492 continue; 1493 } 1494 parts.putAttributeValue(-1, "case", "nominative".equals(otherCase) ? null : otherCase); 1495 String otherValue = cldrFile.getStringValue(parts.toString()); 1496 if (otherValue != null && !otherValue.equals(unitPattern)) { 1497 return otherCase; 1498 } 1499 } 1500 return null; 1501 } 1502 handleFormatPerUnit(String value, List<String> examples)1503 private void handleFormatPerUnit(String value, List<String> examples) { 1504 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(1); 1505 examples.add( 1506 format( 1507 value, 1508 backgroundStartSymbol + numberFormat.format(1) + backgroundEndSymbol)); 1509 } 1510 handleCompoundUnit(XPathParts parts, List<String> examples)1511 public void handleCompoundUnit(XPathParts parts, List<String> examples) { 1512 UnitLength unitLength = getUnitLength(parts); 1513 String compoundType = parts.getAttributeValue(-2, "type"); 1514 Count count = 1515 Count.valueOf(CldrUtility.ifNull(parts.getAttributeValue(-1, "count"), "other")); 1516 handleCompoundUnit(unitLength, compoundType, count, examples); 1517 } 1518 1519 @SuppressWarnings("deprecation") handleCompoundUnit( UnitLength unitLength, String compoundType, Count count, List<String> examples)1520 public void handleCompoundUnit( 1521 UnitLength unitLength, String compoundType, Count count, List<String> examples) { 1522 /* 1523 * <units> 1524 <unitLength type="long"> 1525 <alias source="locale" path="../unitLength[@type='short']"/> 1526 </unitLength> 1527 <unitLength type="short"> 1528 <compoundUnit type="per"> 1529 <unitPattern count="other">{0}/{1}</unitPattern> 1530 </compoundUnit> 1531 1532 * <compoundUnit type="per"> 1533 <unitPattern count="one">{0}/{1}</unitPattern> 1534 <unitPattern count="other">{0}/{1}</unitPattern> 1535 </compoundUnit> 1536 <unit type="length-m"> 1537 <unitPattern count="one">{0} meter</unitPattern> 1538 <unitPattern count="other">{0} meters</unitPattern> 1539 </unit> 1540 1541 */ 1542 1543 // we want to get a number that works for the count passed in. 1544 DecimalQuantity amount = getBest(count); 1545 if (amount == null) { 1546 examples.add("n/a"); 1547 return; 1548 } 1549 DecimalQuantity oneValue = DecimalQuantity_DualStorageBCD.fromExponentString("1"); 1550 1551 String unit1mid; 1552 String unit2mid; 1553 switch (compoundType) { 1554 default: 1555 examples.add("n/a"); 1556 return; 1557 case "per": 1558 unit1mid = getFormattedUnit("length-meter", unitLength, amount); 1559 unit2mid = getFormattedUnit("duration-second", unitLength, oneValue, ""); 1560 break; 1561 case "times": 1562 unit1mid = 1563 getFormattedUnit( 1564 "force-newton", 1565 unitLength, 1566 oneValue, 1567 icuServiceBuilder.getNumberFormat(1).format(amount.toBigDecimal())); 1568 unit2mid = getFormattedUnit("length-meter", unitLength, amount, ""); 1569 break; 1570 } 1571 String unit1 = backgroundStartSymbol + unit1mid.trim() + backgroundEndSymbol; 1572 String unit2 = backgroundStartSymbol + unit2mid.trim() + backgroundEndSymbol; 1573 1574 String form = this.pluralInfo.getPluralRules().select(amount); 1575 // we rebuild a path, because we may have changed it. 1576 String perPath = makeCompoundUnitPath(unitLength, compoundType, "compoundUnitPattern"); 1577 examples.add(format(getValueFromFormat(perPath, form), unit1, unit2)); 1578 } 1579 handleCompoundUnit1( XPathParts parts, String compoundPattern, List<String> examples)1580 public void handleCompoundUnit1( 1581 XPathParts parts, String compoundPattern, List<String> examples) { 1582 UnitLength unitLength = getUnitLength(parts); 1583 String pathCount = parts.getAttributeValue(-1, "count"); 1584 if (pathCount == null) { 1585 handleCompoundUnit1Name(unitLength, compoundPattern, examples); 1586 } else { 1587 handleCompoundUnit1(unitLength, Count.valueOf(pathCount), compoundPattern, examples); 1588 } 1589 } 1590 handleCompoundUnit1Name( UnitLength unitLength, String compoundPattern, List<String> examples)1591 private void handleCompoundUnit1Name( 1592 UnitLength unitLength, String compoundPattern, List<String> examples) { 1593 String pathFormat = 1594 "//ldml/units/unitLength" 1595 + unitLength.typeString 1596 + "/unit[@type=\"{0}\"]/displayName"; 1597 1598 String meterFormat = getValueFromFormat(pathFormat, "length-meter"); 1599 1600 String modFormat = 1601 combinePrefix(meterFormat, compoundPattern, unitLength == UnitLength.LONG); 1602 1603 examples.add(removeEmptyRuns(modFormat)); 1604 } 1605 handleCompoundUnit1( UnitLength unitLength, Count count, String compoundPattern, List<String> examples)1606 public void handleCompoundUnit1( 1607 UnitLength unitLength, Count count, String compoundPattern, List<String> examples) { 1608 1609 // we want to get a number that works for the count passed in. 1610 DecimalQuantity amount = getBest(count); 1611 if (amount == null) { 1612 examples.add("n/a"); 1613 return; 1614 } 1615 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(1); 1616 1617 @SuppressWarnings("deprecation") 1618 String form1 = this.pluralInfo.getPluralRules().select(amount); 1619 1620 String pathFormat = 1621 "//ldml/units/unitLength" 1622 + unitLength.typeString 1623 + "/unit[@type=\"{0}\"]/unitPattern[@count=\"{1}\"]"; 1624 1625 // now pick up the meter pattern 1626 1627 String meterFormat = getValueFromFormat(pathFormat, "length-meter", form1); 1628 1629 // now combine them 1630 1631 String modFormat = 1632 combinePrefix(meterFormat, compoundPattern, unitLength == UnitLength.LONG); 1633 1634 examples.add( 1635 removeEmptyRuns(format(modFormat, numberFormat.format(amount.toBigDecimal())))); 1636 } 1637 1638 // TODO, pass in unitLength instead of last parameter, and do work in Units.combinePattern. 1639 combinePrefix( String unitFormat, String inCompoundPattern, boolean lowercaseUnitIfNoSpaceInCompound)1640 public String combinePrefix( 1641 String unitFormat, String inCompoundPattern, boolean lowercaseUnitIfNoSpaceInCompound) { 1642 // mark the part except for the {0} as foreground 1643 String compoundPattern = 1644 backgroundEndSymbol 1645 + inCompoundPattern.replace( 1646 "{0}", backgroundStartSymbol + "{0}" + backgroundEndSymbol) 1647 + backgroundStartSymbol; 1648 1649 String modFormat = 1650 Units.combinePattern(unitFormat, compoundPattern, lowercaseUnitIfNoSpaceInCompound); 1651 1652 return backgroundStartSymbol + modFormat + backgroundEndSymbol; 1653 } 1654 1655 // ldml/units/unitLength[@type="long"]/compoundUnit[@type="per"]/compoundUnitPattern makeCompoundUnitPath( UnitLength unitLength, String compoundType, String patternType)1656 public String makeCompoundUnitPath( 1657 UnitLength unitLength, String compoundType, String patternType) { 1658 return "//ldml/units/unitLength" 1659 + unitLength.typeString 1660 + "/compoundUnit[@type=\"" 1661 + compoundType 1662 + "\"]" 1663 + "/" 1664 + patternType; 1665 } 1666 1667 @SuppressWarnings("deprecation") getBest(Count count)1668 private DecimalQuantity getBest(Count count) { 1669 DecimalQuantitySamples samples = 1670 pluralInfo.getPluralRules().getDecimalSamples(count.name(), SampleType.DECIMAL); 1671 if (samples == null) { 1672 samples = 1673 pluralInfo.getPluralRules().getDecimalSamples(count.name(), SampleType.INTEGER); 1674 } 1675 if (samples == null) { 1676 return null; 1677 } 1678 Set<DecimalQuantitySamplesRange> samples2 = samples.getSamples(); 1679 DecimalQuantitySamplesRange range = samples2.iterator().next(); 1680 return range.end; 1681 } 1682 handleMiscPatterns(XPathParts parts, String value, List<String> examples)1683 private void handleMiscPatterns(XPathParts parts, String value, List<String> examples) { 1684 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(0); 1685 String start = backgroundStartSymbol + numberFormat.format(99) + backgroundEndSymbol; 1686 if ("range".equals(parts.getAttributeValue(-1, "type"))) { 1687 String end = backgroundStartSymbol + numberFormat.format(144) + backgroundEndSymbol; 1688 examples.add(format(value, start, end)); 1689 } else { 1690 examples.add(format(value, start)); 1691 } 1692 } 1693 handleIntervalFormats(XPathParts parts, String value, List<String> examples)1694 private void handleIntervalFormats(XPathParts parts, String value, List<String> examples) { 1695 if (!parts.getAttributeValue(3, "type").equals("gregorian")) { 1696 return; 1697 } 1698 if (parts.getElement(6).equals("intervalFormatFallback")) { 1699 SimpleDateFormat dateFormat = new SimpleDateFormat(); 1700 String fallbackFormat = invertBackground(setBackground(value)); 1701 examples.add( 1702 format( 1703 fallbackFormat, 1704 dateFormat.format(FIRST_INTERVAL), 1705 dateFormat.format(SECOND_INTERVAL.get("y")))); 1706 return; 1707 } 1708 String greatestDifference = parts.getAttributeValue(-1, "id"); 1709 /* 1710 * Choose an example interval suitable for the symbol. If testing years, use an interval 1711 * of more than one year, and so forth. For the purpose of choosing the interval, 1712 * "H" is equivalent to "h", and so forth; map to a symbol that occurs in SECOND_INTERVAL. 1713 */ 1714 if (greatestDifference.equals("H")) { // Hour [0-23] 1715 greatestDifference = "h"; // Hour [1-12] 1716 } else if (greatestDifference.equals("B") // flexible day periods 1717 || greatestDifference.equals("b")) { // am, pm, noon, midnight 1718 greatestDifference = "a"; // AM, PM 1719 } 1720 // intervalFormatFallback 1721 // //ldml/dates/calendars/calendar[@type="gregorian"]/dateTimeFormats/intervalFormats/intervalFormatItem[@id="yMd"]/greatestDifference[@id="y"] 1722 // find where to split the value 1723 intervalFormat.setPattern(parts, value); 1724 Date later = SECOND_INTERVAL.get(greatestDifference); 1725 if (later == null) { 1726 /* 1727 * This may still happen for some less-frequently used symbols such as "Q" (Quarter), 1728 * if they ever occur in the data. 1729 * Reference: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 1730 * For now, such paths do not get examples. 1731 */ 1732 return; 1733 } 1734 examples.add(intervalFormat.format(FIRST_INTERVAL, later)); 1735 } 1736 handleDelimiters( XPathParts parts, String xpath, String value, List<String> examples)1737 private void handleDelimiters( 1738 XPathParts parts, String xpath, String value, List<String> examples) { 1739 String lastElement = parts.getElement(-1); 1740 final String[] elements = { 1741 "quotationStart", "alternateQuotationStart", 1742 "alternateQuotationEnd", "quotationEnd" 1743 }; 1744 String[] quotes = new String[4]; 1745 String baseXpath = xpath.substring(0, xpath.lastIndexOf('/')); 1746 for (int i = 0; i < quotes.length; i++) { 1747 String currElement = elements[i]; 1748 if (lastElement.equals(currElement)) { 1749 quotes[i] = backgroundStartSymbol + value + backgroundEndSymbol; 1750 } else { 1751 quotes[i] = cldrFile.getWinningValue(baseXpath + '/' + currElement); 1752 } 1753 } 1754 String example = 1755 cldrFile.getStringValue( 1756 "//ldml/localeDisplayNames/types/type[@key=\"calendar\"][@type=\"gregorian\"]"); 1757 // NOTE: the example provided here is partially in English because we don't 1758 // have a translated conversational example in CLDR. 1759 examples.add( 1760 invertBackground( 1761 format("{0}They said {1}" + example + "{2}.{3}", (Object[]) quotes))); 1762 } 1763 handleListPatterns(XPathParts parts, String value, List<String> examples)1764 private void handleListPatterns(XPathParts parts, String value, List<String> examples) { 1765 // listPatternType is either "duration" or null/other list 1766 String listPatternType = parts.getAttributeValue(-2, "type"); 1767 if (listPatternType == null || !listPatternType.contains("unit")) { 1768 handleRegularListPatterns(parts, value, ListTypeLength.from(listPatternType), examples); 1769 } else { 1770 handleDurationListPatterns(parts, value, UnitLength.from(listPatternType), examples); 1771 } 1772 } 1773 handleRegularListPatterns( XPathParts parts, String value, ListTypeLength listTypeLength, List<String> examples)1774 private void handleRegularListPatterns( 1775 XPathParts parts, String value, ListTypeLength listTypeLength, List<String> examples) { 1776 String patternType = parts.getAttributeValue(-1, "type"); 1777 if (patternType == null) { 1778 return; // formerly happened for some "/alias" paths 1779 } 1780 String pathFormat = "//ldml/localeDisplayNames/territories/territory[@type=\"{0}\"]"; 1781 String territory1 = getValueFromFormat(pathFormat, "CH"); 1782 String territory2 = getValueFromFormat(pathFormat, "JP"); 1783 if (patternType.equals("2")) { 1784 examples.add(invertBackground(format(setBackground(value), territory1, territory2))); 1785 return; 1786 } 1787 String territory3 = getValueFromFormat(pathFormat, "EG"); 1788 String territory4 = getValueFromFormat(pathFormat, "CA"); 1789 examples.add( 1790 longListPatternExample( 1791 listTypeLength.getPath(), 1792 patternType, 1793 value, 1794 territory1, 1795 territory2, 1796 territory3, 1797 territory4)); 1798 } 1799 handleDurationListPatterns( XPathParts parts, String value, UnitLength unitWidth, List<String> examples)1800 private void handleDurationListPatterns( 1801 XPathParts parts, String value, UnitLength unitWidth, List<String> examples) { 1802 String patternType = parts.getAttributeValue(-1, "type"); 1803 if (patternType == null) { 1804 return; // formerly happened for some "/alias" paths 1805 } 1806 String duration1 = getFormattedUnit("duration-day", unitWidth, 4); 1807 String duration2 = getFormattedUnit("duration-hour", unitWidth, 2); 1808 if (patternType.equals("2")) { 1809 examples.add(invertBackground(format(setBackground(value), duration1, duration2))); 1810 return; 1811 } 1812 String duration3 = getFormattedUnit("duration-minute", unitWidth, 37); 1813 String duration4 = getFormattedUnit("duration-second", unitWidth, 23); 1814 examples.add( 1815 longListPatternExample( 1816 unitWidth.listTypeLength.getPath(), 1817 patternType, 1818 value, 1819 duration1, 1820 duration2, 1821 duration3, 1822 duration4)); 1823 } 1824 1825 public enum UnitLength { 1826 LONG(ListTypeLength.UNIT_WIDE), 1827 SHORT(ListTypeLength.UNIT_SHORT), 1828 NARROW(ListTypeLength.UNIT_NARROW); 1829 final String typeString; 1830 final ListTypeLength listTypeLength; 1831 UnitLength(ListTypeLength listTypeLength)1832 UnitLength(ListTypeLength listTypeLength) { 1833 typeString = "[@type=\"" + name().toLowerCase(Locale.ENGLISH) + "\"]"; 1834 this.listTypeLength = listTypeLength; 1835 } 1836 from(String listPatternType)1837 public static UnitLength from(String listPatternType) { 1838 switch (listPatternType) { 1839 case "unit": 1840 return UnitLength.LONG; 1841 case "unit-narrow": 1842 return UnitLength.NARROW; 1843 case "unit-short": 1844 return UnitLength.SHORT; 1845 default: 1846 throw new IllegalArgumentException(); 1847 } 1848 } 1849 } 1850 getFormattedUnit( String unitType, UnitLength unitWidth, DecimalQuantity unitAmount)1851 private String getFormattedUnit( 1852 String unitType, UnitLength unitWidth, DecimalQuantity unitAmount) { 1853 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(1); 1854 return getFormattedUnit( 1855 unitType, unitWidth, unitAmount, numberFormat.format(unitAmount.toBigDecimal())); 1856 } 1857 getFormattedUnit(String unitType, UnitLength unitWidth, double unitAmount)1858 private String getFormattedUnit(String unitType, UnitLength unitWidth, double unitAmount) { 1859 return getFormattedUnit( 1860 unitType, unitWidth, new DecimalQuantity_DualStorageBCD(unitAmount)); 1861 } 1862 1863 @SuppressWarnings("deprecation") getFormattedUnit( String unitType, UnitLength unitWidth, DecimalQuantity unitAmount, String formattedUnitAmount)1864 private String getFormattedUnit( 1865 String unitType, 1866 UnitLength unitWidth, 1867 DecimalQuantity unitAmount, 1868 String formattedUnitAmount) { 1869 String form = this.pluralInfo.getPluralRules().select(unitAmount); 1870 String pathFormat = 1871 "//ldml/units/unitLength" 1872 + unitWidth.typeString 1873 + "/unit[@type=\"{0}\"]/unitPattern[@count=\"{1}\"]"; 1874 return format(getValueFromFormat(pathFormat, unitType, form), formattedUnitAmount); 1875 } 1876 1877 // ldml/listPatterns/listPattern/listPatternPart[@type="2"] — And 1878 // ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"] Short And 1879 // ldml/listPatterns/listPattern[@type="or"]/listPatternPart[@type="2"] or list 1880 // ldml/listPatterns/listPattern[@type="unit"]/listPatternPart[@type="2"] 1881 // ldml/listPatterns/listPattern[@type="unit-short"]/listPatternPart[@type="2"] 1882 // ldml/listPatterns/listPattern[@type="unit-narrow"]/listPatternPart[@type="2"] 1883 longListPatternExample( String listPathFormat, String patternType, String value, String... items)1884 private String longListPatternExample( 1885 String listPathFormat, String patternType, String value, String... items) { 1886 String doublePattern = getPattern(listPathFormat, "2", patternType, value); 1887 String startPattern = getPattern(listPathFormat, "start", patternType, value); 1888 String middlePattern = getPattern(listPathFormat, "middle", patternType, value); 1889 String endPattern = getPattern(listPathFormat, "end", patternType, value); 1890 /* 1891 * DateTimePatternGenerator.FormatParser is deprecated, but deprecated in ICU is 1892 * also used to mark internal methods (which are OK for us to use in CLDR). 1893 */ 1894 @SuppressWarnings("deprecation") 1895 ListFormatter listFormatter = 1896 new ListFormatter(doublePattern, startPattern, middlePattern, endPattern); 1897 String example = listFormatter.format((Object[]) items); 1898 return invertBackground(example); 1899 } 1900 1901 /** 1902 * Helper method for handleListPatterns. Returns the pattern to be used for a specified pattern 1903 * type. 1904 * 1905 * @param pathFormat 1906 * @param pathPatternType 1907 * @param valuePatternType 1908 * @param value 1909 * @return 1910 */ getPattern( String pathFormat, String pathPatternType, String valuePatternType, String value)1911 private String getPattern( 1912 String pathFormat, String pathPatternType, String valuePatternType, String value) { 1913 return valuePatternType.equals(pathPatternType) 1914 ? setBackground(value) 1915 : getValueFromFormat(pathFormat, pathPatternType); 1916 } 1917 getValueFromFormat(String format, Object... arguments)1918 private String getValueFromFormat(String format, Object... arguments) { 1919 return cldrFile.getWinningValue(format(format, arguments)); 1920 } 1921 handleEllipsis(String type, String value, List<String> examples)1922 public void handleEllipsis(String type, String value, List<String> examples) { 1923 String pathFormat = "//ldml/localeDisplayNames/territories/territory[@type=\"{0}\"]"; 1924 // <ellipsis type="word-final">{0} …</ellipsis> 1925 // <ellipsis type="word-initial">… {0}</ellipsis> 1926 // <ellipsis type="word-medial">{0} … {1}</ellipsis> 1927 String territory1 = getValueFromFormat(pathFormat, "CH"); 1928 String territory2 = getValueFromFormat(pathFormat, "JP"); 1929 // if it isn't a word, break in the middle 1930 if (!type.contains("word")) { 1931 territory1 = clip(territory1, 0, 1); 1932 territory2 = clip(territory2, 1, 0); 1933 } 1934 if (type.contains("initial")) { 1935 territory1 = territory2; 1936 } 1937 examples.add(invertBackground(format(setBackground(value), territory1, territory2))); 1938 } 1939 clip(String text, int clipStart, int clipEnd)1940 public static String clip(String text, int clipStart, int clipEnd) { 1941 BreakIterator bi = BreakIterator.getCharacterInstance(); 1942 bi.setText(text); 1943 for (int i = 0; i < clipStart; ++i) { 1944 bi.next(); 1945 } 1946 int start = bi.current(); 1947 bi.last(); 1948 for (int i = 0; i < clipEnd; ++i) { 1949 bi.previous(); 1950 } 1951 int end = bi.current(); 1952 return start >= end ? text : text.substring(start, end); 1953 } 1954 1955 /** 1956 * Handle miscellaneous calendar patterns. 1957 * 1958 * @param parts 1959 * @param value 1960 * @return 1961 */ handleMonthPatterns(XPathParts parts, String value, List<String> examples)1962 private void handleMonthPatterns(XPathParts parts, String value, List<String> examples) { 1963 String calendar = parts.getAttributeValue(3, "type"); 1964 String context = parts.getAttributeValue(5, "type"); 1965 String month = "8"; 1966 if (!context.equals("numeric")) { 1967 String width = parts.getAttributeValue(6, "type"); 1968 String xpath = 1969 "//ldml/dates/calendars/calendar[@type=\"{0}\"]/months/monthContext[@type=\"{1}\"]/monthWidth[@type=\"{2}\"]/month[@type=\"8\"]"; 1970 month = getValueFromFormat(xpath, calendar, context, width); 1971 } 1972 examples.add(invertBackground(format(setBackground(value), month))); 1973 } 1974 handleAppendItems(XPathParts parts, String value, List<String> examples)1975 private void handleAppendItems(XPathParts parts, String value, List<String> examples) { 1976 String request = parts.getAttributeValue(-1, "request"); 1977 if (!"Timezone".equals(request)) { 1978 return; 1979 } 1980 String calendar = parts.getAttributeValue(3, "type"); 1981 1982 SimpleDateFormat sdf = 1983 icuServiceBuilder.getDateFormat(calendar, 0, DateFormat.MEDIUM, null); 1984 String zone = cldrFile.getStringValue("//ldml/dates/timeZoneNames/gmtZeroFormat"); 1985 examples.add(format(value, setBackground(sdf.format(DATE_SAMPLE)), setBackground(zone))); 1986 } 1987 1988 private class IntervalFormat { 1989 @SuppressWarnings("deprecation") 1990 DateTimePatternGenerator.FormatParser formatParser = 1991 new DateTimePatternGenerator.FormatParser(); 1992 1993 SimpleDateFormat firstFormat = new SimpleDateFormat(); 1994 SimpleDateFormat secondFormat = new SimpleDateFormat(); 1995 StringBuilder first = new StringBuilder(); 1996 StringBuilder second = new StringBuilder(); 1997 BitSet letters = new BitSet(); 1998 format(Date earlier, Date later)1999 public String format(Date earlier, Date later) { 2000 if (earlier == null || later == null) { 2001 return null; 2002 } 2003 if (later.compareTo(earlier) < 0) { 2004 /* 2005 * Swap so earlier is earlier than later. 2006 * This is necessary for "G" (Era) given the current FIRST_INTERVAL, SECOND_INTERVAL 2007 */ 2008 Date tmp = earlier; 2009 earlier = later; 2010 later = tmp; 2011 } 2012 return firstFormat.format(earlier) + secondFormat.format(later); 2013 } 2014 2015 @SuppressWarnings("deprecation") setPattern(XPathParts parts, String pattern)2016 public IntervalFormat setPattern(XPathParts parts, String pattern) { 2017 if (formatParser == null || pattern == null) { 2018 return this; 2019 } 2020 try { 2021 formatParser.set(pattern); 2022 } catch (NullPointerException e) { 2023 /* 2024 * This has been observed to occur, within ICU, for unknown reasons. 2025 */ 2026 System.err.println( 2027 "Caught NullPointerException in IntervalFormat.setPattern, pattern = " 2028 + pattern); 2029 e.printStackTrace(); 2030 return null; 2031 } 2032 first.setLength(0); 2033 second.setLength(0); 2034 boolean doFirst = true; 2035 letters.clear(); 2036 2037 for (Object item : formatParser.getItems()) { 2038 if (item instanceof DateTimePatternGenerator.VariableField) { 2039 char c = item.toString().charAt(0); 2040 if (letters.get(c)) { 2041 doFirst = false; 2042 } else { 2043 letters.set(c); 2044 } 2045 if (doFirst) { 2046 first.append(item); 2047 } else { 2048 second.append(item); 2049 } 2050 } else { 2051 if (doFirst) { 2052 first.append(formatParser.quoteLiteral((String) item)); 2053 } else { 2054 second.append(formatParser.quoteLiteral((String) item)); 2055 } 2056 } 2057 } 2058 String calendar = parts.findAttributeValue("calendar", "type"); 2059 firstFormat = icuServiceBuilder.getDateFormat(calendar, first.toString()); 2060 firstFormat.setTimeZone(GMT_ZONE_SAMPLE); 2061 2062 secondFormat = icuServiceBuilder.getDateFormat(calendar, second.toString()); 2063 secondFormat.setTimeZone(GMT_ZONE_SAMPLE); 2064 return this; 2065 } 2066 } 2067 handleDurationUnit(String value, List<String> examples)2068 private void handleDurationUnit(String value, List<String> examples) { 2069 DateFormat df = this.icuServiceBuilder.getDateFormat("gregorian", value.replace('h', 'H')); 2070 df.setTimeZone(TimeZone.GMT_ZONE); 2071 long time = ((5 * 60 + 37) * 60 + 23) * 1000; 2072 try { 2073 examples.add(df.format(new Date(time))); 2074 } catch (IllegalArgumentException e) { 2075 // e.g., Illegal pattern character 'o' in "aɖabaƒoƒo m:ss" 2076 return; 2077 } 2078 } 2079 2080 @SuppressWarnings("deprecation") formatCountValue( String xpath, XPathParts parts, String value, List<String> examples)2081 private void formatCountValue( 2082 String xpath, XPathParts parts, String value, List<String> examples) { 2083 if (!parts.containsAttribute("count")) { // no examples for items that don't format 2084 return; 2085 } 2086 final PluralInfo plurals = 2087 supplementalDataInfo.getPlurals(PluralType.cardinal, cldrFile.getLocaleID()); 2088 PluralRules pluralRules = plurals.getPluralRules(); 2089 2090 String unitType = parts.getAttributeValue(-2, "type"); 2091 if (unitType == null) { 2092 unitType = "USD"; // sample for currency pattern 2093 } 2094 final boolean isPattern = parts.contains("unitPattern"); 2095 final boolean isCurrency = !parts.contains("units"); 2096 2097 Count count; 2098 final LinkedHashSet<DecimalQuantity> exampleCount = new LinkedHashSet<>(CURRENCY_SAMPLES); 2099 String countString = parts.getAttributeValue(-1, "count"); 2100 if (countString == null) { 2101 return; 2102 } else { 2103 try { 2104 count = Count.valueOf(countString); 2105 } catch (Exception e) { 2106 return; // counts like 0 2107 } 2108 } 2109 2110 // we used to just get the samples for the given keyword, but that doesn't work well any 2111 // more. 2112 getStartEndSamples( 2113 pluralRules.getDecimalSamples(countString, SampleType.INTEGER), exampleCount); 2114 getStartEndSamples( 2115 pluralRules.getDecimalSamples(countString, SampleType.DECIMAL), exampleCount); 2116 2117 String result = ""; 2118 DecimalFormat currencyFormat = icuServiceBuilder.getCurrencyFormat(unitType); 2119 int decimalCount = currencyFormat.getMinimumFractionDigits(); 2120 2121 // Unless/until DecimalQuantity overrides hashCode() or implements Comparable, we 2122 // should use a concrete collection type for examplesSeen for which .contains() only 2123 // relies on DecimalQuantity.equals() . The reason is that the default hashCode() 2124 // implementation for DecimalQuantity may return false when .equals() returns true. 2125 Collection<DecimalQuantity> examplesSeen = new ArrayList<>(); 2126 2127 // we will cycle until we have (at most) two examples. 2128 int maxCount = 2; 2129 main: 2130 // If we are a currency, we will try to see if we can set the decimals to match. 2131 // but if nothing works, we will just use a plain sample. 2132 for (int phase = 0; phase < 2; ++phase) { 2133 for (DecimalQuantity example : exampleCount) { 2134 // we have to first see whether we have a currency. If so, we have to see if the 2135 // count works. 2136 2137 if (isCurrency && phase == 0) { 2138 DecimalQuantity_DualStorageBCD newExample = 2139 new DecimalQuantity_DualStorageBCD(); 2140 newExample.copyFrom(example); 2141 newExample.setMinFraction(decimalCount); 2142 example = newExample; 2143 } 2144 // skip if we've done before (can happen because of the currency reset) 2145 if (examplesSeen.contains(example)) { 2146 continue; 2147 } 2148 examplesSeen.add(example); 2149 // skip if the count isn't appropriate 2150 if (!pluralRules.select(example).equals(count.toString())) { 2151 continue; 2152 } 2153 2154 if (value == null) { 2155 String fallbackPath = cldrFile.getCountPathWithFallback(xpath, count, true); 2156 value = cldrFile.getStringValue(fallbackPath); 2157 } 2158 String resultItem; 2159 2160 resultItem = formatCurrency(value, unitType, isPattern, isCurrency, count, example); 2161 // now add to list 2162 result = addExampleResult(resultItem, result); 2163 if (isPattern) { 2164 String territory = getDefaultTerritory(); 2165 String currency = supplementalDataInfo.getDefaultCurrency(territory); 2166 if (currency.equals(unitType)) { 2167 currency = "EUR"; 2168 if (currency.equals(unitType)) { 2169 currency = "JAY"; 2170 } 2171 } 2172 resultItem = 2173 formatCurrency(value, currency, isPattern, isCurrency, count, example); 2174 // now add to list 2175 result = addExampleResult(resultItem, result); 2176 } 2177 if (--maxCount < 1) { 2178 break main; 2179 } 2180 } 2181 } 2182 examples.add(result.isEmpty() ? null : result); 2183 } 2184 2185 @SuppressWarnings("deprecation") getStartEndSamples( DecimalQuantitySamples samples, Set<DecimalQuantity> target)2186 public static void getStartEndSamples( 2187 DecimalQuantitySamples samples, Set<DecimalQuantity> target) { 2188 if (samples != null) { 2189 for (DecimalQuantitySamplesRange item : samples.getSamples()) { 2190 target.add(item.start); 2191 target.add(item.end); 2192 } 2193 } 2194 } 2195 2196 @SuppressWarnings("deprecation") formatCurrency( String value, String unitType, final boolean isPattern, final boolean isCurrency, Count count, DecimalQuantity example)2197 private String formatCurrency( 2198 String value, 2199 String unitType, 2200 final boolean isPattern, 2201 final boolean isCurrency, 2202 Count count, 2203 DecimalQuantity example) { 2204 String resultItem; 2205 { 2206 // If we have a pattern, get the unit from the count 2207 // If we have a unit, get the pattern from the count 2208 // English is special; both values are retrieved based on the count. 2209 String unitPattern; 2210 String unitName; 2211 if (isPattern) { 2212 // //ldml/numbers/currencies/currency[@type="USD"]/displayName 2213 unitName = getUnitName(unitType, isCurrency, count); 2214 unitPattern = typeIsEnglish ? getUnitPattern(unitType, isCurrency, count) : value; 2215 } else { 2216 unitPattern = getUnitPattern(unitType, isCurrency, count); 2217 unitName = typeIsEnglish ? getUnitName(unitType, isCurrency, count) : value; 2218 } 2219 2220 if (isPattern) { 2221 unitPattern = setBackground(unitPattern); 2222 } else { 2223 unitPattern = setBackgroundExceptMatch(unitPattern, PARAMETER_SKIP0); 2224 } 2225 2226 MessageFormat unitPatternFormat = new MessageFormat(unitPattern); 2227 2228 // get the format for the currency 2229 // TODO fix this for special currency overrides 2230 2231 DecimalFormat unitDecimalFormat = icuServiceBuilder.getNumberFormat(1); // decimal 2232 unitDecimalFormat.setMaximumFractionDigits((int) example.getPluralOperand(Operand.v)); 2233 unitDecimalFormat.setMinimumFractionDigits((int) example.getPluralOperand(Operand.v)); 2234 2235 String formattedNumber = unitDecimalFormat.format(example.toDouble()); 2236 unitPatternFormat.setFormatByArgumentIndex(0, unitDecimalFormat); 2237 resultItem = unitPattern.replace("{0}", formattedNumber).replace("{1}", unitName); 2238 2239 if (isPattern) { 2240 resultItem = invertBackground(resultItem); 2241 } 2242 } 2243 return resultItem; 2244 } 2245 addExampleResult(String resultItem, String resultToAddTo)2246 private String addExampleResult(String resultItem, String resultToAddTo) { 2247 return addExampleResult(resultItem, resultToAddTo, false); 2248 } 2249 addExampleResult(String resultItem, String resultToAddTo, boolean showContexts)2250 private String addExampleResult(String resultItem, String resultToAddTo, boolean showContexts) { 2251 if (!showContexts) { 2252 if (resultToAddTo.length() != 0) { 2253 resultToAddTo += exampleSeparatorSymbol; 2254 } 2255 resultToAddTo += resultItem; 2256 } else { 2257 resultToAddTo += 2258 exampleStartAutoSymbol 2259 + resultItem 2260 + exampleEndSymbol; // example in neutral context 2261 resultToAddTo += 2262 exampleStartRTLSymbol + resultItem + exampleEndSymbol; // example in RTL context 2263 } 2264 return resultToAddTo; 2265 } 2266 getUnitPattern(String unitType, final boolean isCurrency, Count count)2267 private String getUnitPattern(String unitType, final boolean isCurrency, Count count) { 2268 return cldrFile.getStringValue( 2269 isCurrency 2270 ? "//ldml/numbers/currencyFormats/unitPattern" + countAttribute(count) 2271 : "//ldml/units/unit[@type=\"" 2272 + unitType 2273 + "\"]/unitPattern" 2274 + countAttribute(count)); 2275 } 2276 getUnitName(String unitType, final boolean isCurrency, Count count)2277 private String getUnitName(String unitType, final boolean isCurrency, Count count) { 2278 return cldrFile.getStringValue( 2279 isCurrency 2280 ? "//ldml/numbers/currencies/currency[@type=\"" 2281 + unitType 2282 + "\"]/displayName" 2283 + countAttribute(count) 2284 : "//ldml/units/unit[@type=\"" 2285 + unitType 2286 + "\"]/unitPattern" 2287 + countAttribute(count)); 2288 } 2289 countAttribute(Count count)2290 public String countAttribute(Count count) { 2291 return "[@count=\"" + count + "\"]"; 2292 } 2293 handleMinimumGrouping(XPathParts parts, String value, List<String> examples)2294 private void handleMinimumGrouping(XPathParts parts, String value, List<String> examples) { 2295 String numberSystem = cldrFile.getWinningValue("//ldml/numbers/defaultNumberingSystem"); 2296 String checkPath = 2297 "//ldml/numbers/decimalFormats[@numberSystem=\"" 2298 + numberSystem 2299 + "\"]/decimalFormatLength/decimalFormat[@type=\"standard\"]/pattern[@type=\"standard\"]"; 2300 String decimalFormat = cldrFile.getWinningValue(checkPath); 2301 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(decimalFormat, numberSystem); 2302 numberFormat.setMinimumGroupingDigits(Integer.parseInt(value)); 2303 2304 double sampleNum1 = 543.21; 2305 double sampleNum2 = 6543.21; 2306 double sampleNum3 = 76543.21; 2307 examples.add(formatNumber(numberFormat, sampleNum1)); 2308 examples.add(formatNumber(numberFormat, sampleNum2)); 2309 examples.add(formatNumber(numberFormat, sampleNum3)); 2310 } 2311 handleNumberSymbol(XPathParts parts, String value, List<String> examples)2312 private void handleNumberSymbol(XPathParts parts, String value, List<String> examples) { 2313 String symbolType = parts.getElement(-1); 2314 String numberSystem = parts.getAttributeValue(2, "numberSystem"); // null if not present 2315 int index; // dec/percent/sci 2316 double numberSample = NUMBER_SAMPLE; 2317 String originalValue = cldrFile.getWinningValue(parts.toString()); 2318 boolean isSuperscripting = false; 2319 if (symbolType.equals("decimal") || symbolType.equals("group")) { 2320 index = 1; 2321 } else if (symbolType.equals("minusSign")) { 2322 index = 1; 2323 numberSample = -numberSample; 2324 } else if (symbolType.equals("percentSign")) { 2325 // For the perMille symbol, we reuse the percent example. 2326 index = 2; 2327 numberSample = 0.23; 2328 } else if (symbolType.equals("perMille")) { 2329 // For the perMille symbol, we reuse the percent example. 2330 index = 2; 2331 numberSample = 0.023; 2332 originalValue = 2333 cldrFile.getWinningValue(parts.addRelative("../percentSign").toString()); 2334 } else if (symbolType.equals("approximatelySign")) { 2335 // Substitute the approximately symbol in for the minus sign. 2336 index = 1; 2337 numberSample = -numberSample; 2338 originalValue = cldrFile.getWinningValue(parts.addRelative("../minusSign").toString()); 2339 } else if (symbolType.equals("exponential") || symbolType.equals("plusSign")) { 2340 index = 3; 2341 } else if (symbolType.equals("superscriptingExponent")) { 2342 index = 3; 2343 isSuperscripting = true; 2344 } else { 2345 // We don't need examples for standalone symbols, i.e. infinity and nan. 2346 // We don't have an example for the list symbol either. 2347 return; 2348 } 2349 DecimalFormat x = icuServiceBuilder.getNumberFormat(index, numberSystem); 2350 String example; 2351 String formattedValue; 2352 if (isSuperscripting) { 2353 DecimalFormatSymbols symbols = x.getDecimalFormatSymbols(); 2354 char[] digits = symbols.getDigits(); 2355 x.setDecimalFormatSymbols(symbols); 2356 x.setNegativeSuffix(endSupSymbol + x.getNegativeSuffix()); 2357 x.setPositiveSuffix(endSupSymbol + x.getPositiveSuffix()); 2358 x.setExponentSignAlwaysShown(false); 2359 2360 // Don't set the exponent directly because future examples for items 2361 // will be affected as well. 2362 originalValue = symbols.getExponentSeparator(); 2363 formattedValue = 2364 backgroundEndSymbol 2365 + value 2366 + digits[1] 2367 + digits[0] 2368 + backgroundStartSymbol 2369 + startSupSymbol; 2370 } else { 2371 x.setExponentSignAlwaysShown(true); 2372 formattedValue = backgroundEndSymbol + value + backgroundStartSymbol; 2373 } 2374 example = x.format(numberSample); 2375 example = example.replace(originalValue, formattedValue); 2376 examples.add(backgroundStartSymbol + example + backgroundEndSymbol); 2377 } 2378 handleNumberingSystem(String value, List<String> examples)2379 private void handleNumberingSystem(String value, List<String> examples) { 2380 NumberFormat x = icuServiceBuilder.getGenericNumberFormat(value); 2381 x.setGroupingUsed(false); 2382 examples.add(x.format(NUMBER_SAMPLE_WHOLE)); 2383 } 2384 handleTimeZoneName(XPathParts parts, String value, List<String> examples)2385 private void handleTimeZoneName(XPathParts parts, String value, List<String> examples) { 2386 String result = null; 2387 if (parts.contains("exemplarCity")) { 2388 // ldml/dates/timeZoneNames/zone[@type="America/Los_Angeles"]/exemplarCity 2389 String timezone = parts.getAttributeValue(3, "type"); 2390 String countryCode = supplementalDataInfo.getZone_territory(timezone); 2391 if (countryCode == null) { 2392 if (value == null) { 2393 result = timezone.substring(timezone.lastIndexOf('/') + 1).replace('_', ' '); 2394 } else { 2395 result = value; // trivial -- is this beneficial? 2396 } 2397 examples.add(result); 2398 return; 2399 } 2400 if (countryCode.equals("001")) { 2401 // GMT code, so format. 2402 try { 2403 String hourOffset = timezone.substring(timezone.contains("+") ? 8 : 7); 2404 int hours = Integer.parseInt(hourOffset); 2405 result = getGMTFormat(null, null, hours); 2406 } catch (RuntimeException e) { 2407 return; // fail, skip 2408 } 2409 } else { 2410 result = setBackground(cldrFile.getName(CLDRFile.TERRITORY_NAME, countryCode)); 2411 } 2412 } else if (parts.contains("zone")) { // {0} Time 2413 result = value; // trivial -- is this beneficial? 2414 } else if (parts.contains("regionFormat")) { // {0} Time 2415 result = format(value, setBackground(cldrFile.getName(CLDRFile.TERRITORY_NAME, "JP"))); 2416 result = 2417 addExampleResult( 2418 format( 2419 value, 2420 setBackground( 2421 cldrFile.getWinningValue(EXEMPLAR_CITY_LOS_ANGELES))), 2422 result); 2423 } else if (parts.contains("fallbackFormat")) { // {1} ({0}) 2424 String central = 2425 setBackground( 2426 cldrFile.getWinningValue( 2427 "//ldml/dates/timeZoneNames/metazone[@type=\"America_Central\"]/long/generic")); 2428 String cancun = 2429 setBackground( 2430 cldrFile.getWinningValue( 2431 "//ldml/dates/timeZoneNames/zone[@type=\"America/Cancun\"]/exemplarCity")); 2432 result = format(value, cancun, central); 2433 } else if (parts.contains("gmtFormat")) { // GMT{0} 2434 result = getGMTFormat(null, value, -8); 2435 } else if (parts.contains("hourFormat")) { // +HH:mm;-HH:mm 2436 result = getGMTFormat(value, null, -8); 2437 } else if (parts.contains("metazone") 2438 && !parts.contains("commonlyUsed")) { // Metazone string 2439 if (value != null && value.length() > 0) { 2440 result = getMZTimeFormat() + " " + value; 2441 } else { 2442 // TODO check for value 2443 if (parts.contains("generic")) { 2444 String metazone_name = parts.getAttributeValue(3, "type"); 2445 String timezone = 2446 supplementalDataInfo.getZoneForMetazoneByRegion(metazone_name, "001"); 2447 String countryCode = supplementalDataInfo.getZone_territory(timezone); 2448 String regionFormat = 2449 cldrFile.getWinningValue("//ldml/dates/timeZoneNames/regionFormat"); 2450 String countryName = 2451 cldrFile.getWinningValue( 2452 "//ldml/localeDisplayNames/territories/territory[@type=\"" 2453 + countryCode 2454 + "\"]"); 2455 result = 2456 setBackground( 2457 getMZTimeFormat() + " " + format(regionFormat, countryName)); 2458 } else { 2459 String gmtFormat = 2460 cldrFile.getWinningValue("//ldml/dates/timeZoneNames/gmtFormat"); 2461 String hourFormat = 2462 cldrFile.getWinningValue("//ldml/dates/timeZoneNames/hourFormat"); 2463 String metazone_name = parts.getAttributeValue(3, "type"); 2464 String tz_string = 2465 supplementalDataInfo.getZoneForMetazoneByRegion(metazone_name, "001"); 2466 TimeZone currentZone = TimeZone.getTimeZone(tz_string); 2467 int tzOffset = currentZone.getRawOffset(); 2468 if (parts.contains("daylight")) { 2469 tzOffset += currentZone.getDSTSavings(); 2470 } 2471 long tm_hrs = tzOffset / DateConstants.MILLIS_PER_HOUR; 2472 long tm_mins = 2473 (tzOffset % DateConstants.MILLIS_PER_HOUR) 2474 / DateConstants.MILLIS_PER_MINUTE; 2475 result = 2476 setBackground( 2477 getMZTimeFormat() 2478 + " " 2479 + getGMTFormat( 2480 hourFormat, 2481 gmtFormat, 2482 (int) tm_hrs, 2483 (int) tm_mins)); 2484 } 2485 } 2486 } 2487 examples.add(result); 2488 } 2489 2490 @SuppressWarnings("deprecation") handleDateFormatItem( String xpath, String value, boolean showContexts, List<String> examples)2491 private void handleDateFormatItem( 2492 String xpath, String value, boolean showContexts, List<String> examples) { 2493 // Get here if parts contains "calendar" and either of "pattern", "dateFormatItem" 2494 2495 String fullpath = cldrFile.getFullXPath(xpath); 2496 XPathParts parts = XPathParts.getFrozenInstance(fullpath); 2497 String calendar = parts.findAttributeValue("calendar", "type"); 2498 2499 if (parts.contains("dateTimeFormat")) { // date-time combining patterns 2500 // ldml/dates/calendars/calendar[@type="*"]/dateTimeFormats/dateTimeFormatLength[@type="*"]/dateTimeFormat[@type="standard"]/pattern[@type="standard"] 2501 // ldml/dates/calendars/calendar[@type="*"]/dateTimeFormats/dateTimeFormatLength[@type="*"]/dateTimeFormat[@type="atTime"]/pattern[@type="standard"] 2502 String formatType = 2503 parts.findAttributeValue("dateTimeFormat", "type"); // "standard" or "atTime" 2504 String length = 2505 parts.findAttributeValue( 2506 "dateTimeFormatLength", "type"); // full, long, medium, short 2507 2508 // For all types, show 2509 // - date (of same length) with a single full time, or long time (abbreviated zone) if 2510 // the date is short 2511 // - date (of same length) with a single short time 2512 // For the standard patterns, add 2513 // - date (of same length) with a short time range 2514 // - relative date with a short time range 2515 // For the atTime patterns, add 2516 // - relative date with a single short time 2517 2518 // ldml/dates/calendars/calendar[@type="*"]/dateFormats/dateFormatLength[@type="*"]/dateFormat[@type="standard"]/pattern[@type="standard"] 2519 // ldml/dates/calendars/calendar[@type="*"]/dateFormats/dateFormatLength[@type="*"]/dateFormat[@type="standard"]/pattern[@type="standard"][@numbers="*"] 2520 SimpleDateFormat df = cldrFile.getDateFormat(calendar, length, icuServiceBuilder); 2521 df.setTimeZone(ZONE_SAMPLE); 2522 2523 // ldml/dates/calendars/calendar[@type="*"]/timeFormats/timeFormatLength[@type="*"]/timeFormat[@type="standard"]/pattern[@type="standard"] 2524 // ldml/dates/calendars/calendar[@type="*"]/timeFormats/timeFormatLength[@type="*"]/timeFormat[@type="standard"]/pattern[@type="standard"][@numbers="*"] // not currently used 2525 SimpleDateFormat tlf = 2526 (!length.equals("short")) 2527 ? cldrFile.getTimeFormat(calendar, "full", icuServiceBuilder) 2528 : cldrFile.getTimeFormat(calendar, "long", icuServiceBuilder); 2529 2530 if (tlf == null) { 2531 return; 2532 } 2533 2534 tlf.setTimeZone(ZONE_SAMPLE); 2535 2536 SimpleDateFormat tsf = cldrFile.getTimeFormat(calendar, "short", icuServiceBuilder); 2537 tsf.setTimeZone(ZONE_SAMPLE); 2538 2539 // ldml/dates/fields/field[@type="day"]/relative[@type="0"] // "today" 2540 String relativeDayXPath = 2541 cldrFile.getWinningPath( 2542 "//ldml/dates/fields/field[@type=\"day\"]/relative[@type=\"0\"]"); 2543 String relativeDayValue = cldrFile.getWinningValue(relativeDayXPath); 2544 2545 String dfResult = df.format(DATE_SAMPLE); 2546 String tlfResult = tlf.format(DATE_SAMPLE); 2547 String tsfResult = tsf.format(DATE_SAMPLE); // DATE_SAMPLE is in the afternoon 2548 2549 // Handle date plus a single full time. 2550 // We need to process the dateTimePattern as a date pattern (not only a message format) 2551 // so 2552 // we handle it with SimpleDateFormat, plugging the date and time formats in as literal 2553 // text. 2554 examples.add( 2555 cldrFile.glueDateTimeFormatWithGluePattern( 2556 setBackground(dfResult), 2557 setBackground(tlfResult), 2558 calendar, 2559 value, 2560 icuServiceBuilder)); 2561 2562 // Handle date plus a single short time. 2563 examples.add( 2564 cldrFile.glueDateTimeFormatWithGluePattern( 2565 setBackground(dfResult), 2566 setBackground(tsfResult), 2567 calendar, 2568 value, 2569 icuServiceBuilder)); 2570 2571 if (!formatType.contentEquals("atTime")) { 2572 // Examples for standard pattern 2573 2574 // Create a time range (from morning to afternoon, using short time formats). For 2575 // simplicity we format 2576 // using the intervalFormatFallback pattern (should be reasonable for time range 2577 // morning to evening). 2578 int dtfLengthOffset = xpath.indexOf("dateTimeFormatLength"); 2579 if (dtfLengthOffset > 0) { 2580 String intervalFormatFallbackXPath = 2581 xpath.substring(0, dtfLengthOffset) 2582 .concat("intervalFormats/intervalFormatFallback"); 2583 String intervalFormatFallbackValue = 2584 cldrFile.getWinningValue(intervalFormatFallbackXPath); 2585 String tsfAMResult = tsf.format(DATE_SAMPLE3); // DATE_SAMPLE3 is in the morning 2586 String timeRange = format(intervalFormatFallbackValue, tsfAMResult, tsfResult); 2587 2588 // Handle date plus short time range 2589 2590 examples.add( 2591 cldrFile.glueDateTimeFormatWithGluePattern( 2592 setBackground(dfResult), 2593 setBackground(timeRange), 2594 calendar, 2595 value, 2596 icuServiceBuilder)); 2597 2598 // Handle relative date plus short time range 2599 examples.add( 2600 cldrFile.glueDateTimeFormatWithGluePattern( 2601 setBackground(relativeDayValue), 2602 setBackground(timeRange), 2603 calendar, 2604 value, 2605 icuServiceBuilder)); 2606 } 2607 } else { 2608 // Examples for atTime pattern 2609 2610 // Handle relative date plus a single short time. 2611 examples.add( 2612 cldrFile.glueDateTimeFormatWithGluePattern( 2613 setBackground(relativeDayValue), 2614 setBackground(tsfResult), 2615 calendar, 2616 value, 2617 icuServiceBuilder)); 2618 } 2619 2620 return; 2621 } else { 2622 String id = parts.findAttributeValue("dateFormatItem", "id"); 2623 if ("NEW".equals(id) || value == null) { 2624 examples.add(startItalicSymbol + "n/a" + endItalicSymbol); 2625 return; 2626 } else { 2627 String numbersOverride = parts.findAttributeValue("pattern", "numbers"); 2628 SimpleDateFormat sdf = 2629 icuServiceBuilder.getDateFormat(calendar, value, numbersOverride); 2630 String defaultNumberingSystem = 2631 cldrFile.getWinningValue("//ldml/numbers/defaultNumberingSystem"); 2632 String timeSeparator = 2633 cldrFile.getWinningValue( 2634 "//ldml/numbers/symbols[@numberSystem='" 2635 + defaultNumberingSystem 2636 + "']/timeSeparator"); 2637 DateFormatSymbols dfs = sdf.getDateFormatSymbols(); 2638 dfs.setTimeSeparatorString(timeSeparator); 2639 sdf.setDateFormatSymbols(dfs); 2640 if (id == null || id.indexOf('B') < 0) { 2641 sdf.setTimeZone(ZONE_SAMPLE); 2642 // Standard date/time format, or availableFormat without dayPeriod 2643 if (value.contains("MMM") || value.contains("LLL")) { 2644 // alpha month, do not need context examples 2645 examples.add(sdf.format(DATE_SAMPLE)); 2646 return; 2647 } else { 2648 // Use contextExamples if showContexts T 2649 String example = 2650 showContexts 2651 ? exampleStartHeaderSymbol 2652 + contextheader 2653 + exampleEndSymbol 2654 : ""; 2655 String sup_twelve_example = sdf.format(DATE_SAMPLE); 2656 String sub_ten_example = sdf.format(DATE_SAMPLE5); 2657 example = addExampleResult(sup_twelve_example, example, showContexts); 2658 if (!sup_twelve_example.equals(sub_ten_example)) { 2659 example = addExampleResult(sub_ten_example, example, showContexts); 2660 } 2661 examples.add(example); 2662 return; 2663 } 2664 } else { 2665 DayPeriodInfo dayPeriodInfo = 2666 supplementalDataInfo.getDayPeriods( 2667 DayPeriodInfo.Type.format, cldrFile.getLocaleID()); 2668 Set<DayPeriod> dayPeriods = 2669 new LinkedHashSet<DayPeriod>(dayPeriodInfo.getPeriods()); 2670 for (DayPeriod dayPeriod : dayPeriods) { 2671 if (dayPeriod.equals( 2672 DayPeriod.midnight)) { // suppress midnight, see ICU-12278 bug 2673 continue; 2674 } 2675 R3<Integer, Integer, Boolean> info = 2676 dayPeriodInfo.getFirstDayPeriodInfo(dayPeriod); 2677 if (info != null) { 2678 int time = 2679 ((info.get0() + info.get1()) 2680 / 2); // dayPeriod endpoints overlap, midpoint to 2681 // disambiguate 2682 String formatted = sdf.format(time); 2683 examples.add(formatted); 2684 } 2685 } 2686 return; 2687 } 2688 } 2689 } 2690 } 2691 2692 // Simple check whether the currency symbol has letters on one or both sides symbolIsLetters(String currencySymbol, boolean onBothSides)2693 private boolean symbolIsLetters(String currencySymbol, boolean onBothSides) { 2694 int len = currencySymbol.length(); 2695 if (len == 0) { 2696 return false; 2697 } 2698 int limitChar = currencySymbol.codePointAt(0); 2699 if (UCharacter.isLetter(limitChar)) { 2700 if (!onBothSides) { 2701 return true; 2702 } 2703 } else if (onBothSides) { 2704 return false; 2705 } 2706 if (len > 1) { 2707 limitChar = currencySymbol.codePointAt(len - 1); 2708 return UCharacter.isLetter(limitChar); 2709 } 2710 return false; 2711 } 2712 2713 /** 2714 * Creates examples for currency formats. 2715 * 2716 * @param value 2717 * @return 2718 */ handleCurrencyFormat( XPathParts parts, String value, boolean showContexts, List<String> examples)2719 private void handleCurrencyFormat( 2720 XPathParts parts, String value, boolean showContexts, List<String> examples) { 2721 2722 String example = 2723 showContexts ? exampleStartHeaderSymbol + contextheader + exampleEndSymbol : ""; 2724 String territory = getDefaultTerritory(); 2725 2726 String currency = supplementalDataInfo.getDefaultCurrency(territory); 2727 String checkPath = "//ldml/numbers/currencies/currency[@type=\"" + currency + "\"]/symbol"; 2728 String currencySymbol = cldrFile.getWinningValue(checkPath); 2729 String altValue = parts.getAttributeValue(-1, "alt"); 2730 boolean altAlpha = (altValue != null && altValue.equals("alphaNextToNumber")); 2731 if (altAlpha && !symbolIsLetters(currencySymbol, true)) { 2732 // If this example is for alt="alphaNextToNumber" and the default currency symbol 2733 // does not have letters on both sides, need to use a fully alphabetic one. 2734 currencySymbol = currency; 2735 } 2736 2737 String numberSystem = parts.getAttributeValue(2, "numberSystem"); // null if not present 2738 2739 DecimalFormat df = 2740 icuServiceBuilder.getCurrencyFormat(currency, currencySymbol, numberSystem); 2741 df.applyPattern(value); 2742 2743 String countValue = parts.getAttributeValue(-1, "count"); 2744 if (countValue != null) { 2745 examples.add(formatCountDecimal(df, countValue)); 2746 return; 2747 } 2748 2749 double sampleAmount = 1295.00; 2750 example = addExampleResult(formatNumber(df, sampleAmount), example, showContexts); 2751 example = addExampleResult(formatNumber(df, -sampleAmount), example, showContexts); 2752 2753 if (showContexts && !altAlpha) { 2754 // If this example is not for alt="alphaNextToNumber", then if the currency symbol 2755 // above has letters (strong dir) add another example with non-letter symbol 2756 // (weak or neutral), or vice versa 2757 if (symbolIsLetters(currencySymbol, false)) { 2758 currency = "EUR"; 2759 checkPath = "//ldml/numbers/currencies/currency[@type=\"" + currency + "\"]/symbol"; 2760 currencySymbol = cldrFile.getWinningValue(checkPath); 2761 } else { 2762 currencySymbol = currency; 2763 } 2764 df = icuServiceBuilder.getCurrencyFormat(currency, currencySymbol, numberSystem); 2765 df.applyPattern(value); 2766 example = addExampleResult(formatNumber(df, sampleAmount), example, showContexts); 2767 example = addExampleResult(formatNumber(df, -sampleAmount), example, showContexts); 2768 } 2769 2770 examples.add(example); 2771 } 2772 getDefaultTerritory()2773 private String getDefaultTerritory() { 2774 CLDRLocale loc; 2775 String territory = "US"; 2776 if (!typeIsEnglish) { 2777 loc = CLDRLocale.getInstance(cldrFile.getLocaleID()); 2778 territory = loc.getCountry(); 2779 if (territory == null || territory.length() == 0) { 2780 loc = supplementalDataInfo.getDefaultContentFromBase(loc); 2781 if (loc != null) { 2782 territory = loc.getCountry(); 2783 if (territory.equals("001") && loc.getLanguage().equals("ar")) { 2784 territory = 2785 "EG"; // Use Egypt as territory for examples in ar locale, since its 2786 // default content is ar_001. 2787 } 2788 } 2789 } 2790 if (territory == null || territory.length() == 0) { 2791 territory = "US"; 2792 } 2793 } 2794 return territory; 2795 } 2796 2797 /** 2798 * Creates examples for decimal formats. 2799 * 2800 * @param value 2801 * @return 2802 */ handleDecimalFormat( XPathParts parts, String value, boolean showContexts, List<String> examples)2803 private void handleDecimalFormat( 2804 XPathParts parts, String value, boolean showContexts, List<String> examples) { 2805 String example = 2806 showContexts ? exampleStartHeaderSymbol + contextheader + exampleEndSymbol : ""; 2807 String numberSystem = parts.getAttributeValue(2, "numberSystem"); // null if not present 2808 DecimalFormat numberFormat = icuServiceBuilder.getNumberFormat(value, numberSystem); 2809 String countValue = parts.getAttributeValue(-1, "count"); 2810 if (countValue != null) { 2811 examples.add(formatCountDecimal(numberFormat, countValue)); 2812 return; 2813 } 2814 2815 double sampleNum1 = 5.43; 2816 double sampleNum2 = NUMBER_SAMPLE; 2817 if (parts.getElement(4).equals("percentFormat")) { 2818 sampleNum1 = 0.0543; 2819 } 2820 example = addExampleResult(formatNumber(numberFormat, sampleNum1), example, showContexts); 2821 example = addExampleResult(formatNumber(numberFormat, sampleNum2), example, showContexts); 2822 // have positive and negative 2823 example = addExampleResult(formatNumber(numberFormat, -sampleNum2), example, showContexts); 2824 examples.add(example); 2825 } 2826 formatCountDecimal(DecimalFormat numberFormat, String countValue)2827 private String formatCountDecimal(DecimalFormat numberFormat, String countValue) { 2828 Count count; 2829 try { 2830 count = Count.valueOf(countValue); 2831 } catch (Exception e) { 2832 String locale = getCldrFile().getLocaleID(); 2833 PluralInfo pluralInfo = supplementalDataInfo.getPlurals(locale); 2834 count = 2835 pluralInfo.getCount( 2836 DecimalQuantity_DualStorageBCD.fromExponentString(countValue)); 2837 } 2838 Double numberSample = getExampleForPattern(numberFormat, count); 2839 if (numberSample == null) { 2840 // Ideally, we would suppress the value in the survey tool. 2841 // However, until we switch over to the ICU samples, we are not guaranteed 2842 // that "no samples" means "can't occur". So we manufacture something. 2843 int digits = numberFormat.getMinimumIntegerDigits(); 2844 numberSample = (double) Math.round(1.2345678901234 * Math.pow(10, digits - 1)); 2845 } 2846 String temp = String.valueOf(numberSample); 2847 int fractionLength = temp.endsWith(".0") ? 0 : temp.length() - temp.indexOf('.') - 1; 2848 if (fractionLength != numberFormat.getMaximumFractionDigits()) { 2849 numberFormat = (DecimalFormat) numberFormat.clone(); // for safety 2850 numberFormat.setMinimumFractionDigits(fractionLength); 2851 numberFormat.setMaximumFractionDigits(fractionLength); 2852 } 2853 return formatNumber(numberFormat, numberSample); 2854 } 2855 formatNumber(DecimalFormat format, double value)2856 private String formatNumber(DecimalFormat format, double value) { 2857 String example = format.format(value); 2858 return setBackgroundOnMatch(example, ALL_DIGITS); 2859 } 2860 2861 /** 2862 * Calculates a numerical example to use for the specified pattern using brute force (there 2863 * should be a more elegant way to do this). 2864 * 2865 * @param format 2866 * @param count 2867 * @return 2868 */ getExampleForPattern(DecimalFormat format, Count count)2869 private Double getExampleForPattern(DecimalFormat format, Count count) { 2870 if (patternExamples == null) { 2871 patternExamples = PluralSamples.getInstance(cldrFile.getLocaleID()); 2872 } 2873 int numDigits = format.getMinimumIntegerDigits(); 2874 Map<Count, Double> samples = patternExamples.getSamples(numDigits); 2875 if (samples == null) { 2876 return null; 2877 } 2878 return samples.get(count); 2879 } 2880 handleCurrency( String xpath, XPathParts parts, String value, List<String> examples)2881 private void handleCurrency( 2882 String xpath, XPathParts parts, String value, List<String> examples) { 2883 String currency = parts.getAttributeValue(-2, "type"); 2884 String fullPath = cldrFile.getFullXPath(xpath, false); 2885 if (parts.contains("symbol")) { 2886 if (fullPath != null && fullPath.contains("[@choice=\"true\"]")) { 2887 ChoiceFormat cf = new ChoiceFormat(value); 2888 value = cf.format(NUMBER_SAMPLE); 2889 } 2890 String result; 2891 if (value == null) { 2892 throw new NullPointerException( 2893 cldrFile.getSourceLocation(fullPath) 2894 + ": " 2895 + cldrFile.getLocaleID() 2896 + ": " 2897 + ": Error: no currency symbol for " 2898 + currency); 2899 } 2900 DecimalFormat x = icuServiceBuilder.getCurrencyFormat(currency, value); 2901 result = x.format(NUMBER_SAMPLE); 2902 result = 2903 setBackground(result) 2904 .replace(value, backgroundEndSymbol + value + backgroundStartSymbol); 2905 examples.add(result); 2906 } else if (parts.contains("displayName")) { 2907 formatCountValue(xpath, parts, value, examples); 2908 } 2909 return; 2910 } 2911 handleDateRangePattern(String value, List<String> examples)2912 private void handleDateRangePattern(String value, List<String> examples) { 2913 SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian", 2, 0); 2914 examples.add( 2915 format( 2916 value, 2917 setBackground(dateFormat.format(DATE_SAMPLE)), 2918 setBackground(dateFormat.format(DATE_SAMPLE2)))); 2919 } 2920 2921 /** 2922 * Add examples for eras. First checks if there is info for this calendar type and this era type 2923 * in the CALENDAR_ERAS map, then generates a sample date based on this info and formats it 2924 */ handleEras(XPathParts parts, String value, List<String> examples)2925 private void handleEras(XPathParts parts, String value, List<String> examples) { 2926 String calendarId = parts.getAttributeValue(3, "type"); 2927 String type = parts.getAttributeValue(-1, "type"); 2928 String id = 2929 (calendarId.startsWith("islamic")) 2930 ? "islamic" 2931 : calendarId; // islamic variations map to same sample 2932 if (!CALENDAR_ERAS.containsKey(id)) { 2933 return; 2934 } 2935 int typeIndex = Integer.parseInt(type); 2936 if (calendarId.equals("japanese")) { 2937 if (typeIndex < 235) { // examples only for 2 most recent eras 2938 return; 2939 } else { 2940 typeIndex %= 235; // map to length 2 list 2941 } 2942 } 2943 List<Date> eraDates = CALENDAR_ERAS.get(id); 2944 Date sample = eraDates.get(typeIndex); 2945 String skeleton = "Gy"; 2946 String checkPath = 2947 "//ldml/dates/calendars/calendar[@type=\"" 2948 + calendarId 2949 + "\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" 2950 + skeleton 2951 + "\"]"; 2952 String dateFormat = cldrFile.getWinningValue(checkPath); 2953 SimpleDateFormat sdf = icuServiceBuilder.getDateFormat(calendarId, dateFormat); 2954 DateFormatSymbols dfs = sdf.getDateFormatSymbols(); 2955 String[] eraNames = dfs.getEras(); 2956 eraNames[typeIndex] = value; // overwrite era to current value 2957 dfs.setEras(eraNames); 2958 sdf.setDateFormatSymbols(dfs); 2959 examples.add(sdf.format(sample)); 2960 } 2961 2962 /** 2963 * Add examples for quarters for the gregorian calendar, matching each quarter type (1, 2, 3, 4) 2964 * to a corresponding sample month and formatting an example with that date 2965 */ handleQuarters(XPathParts parts, String value, List<String> examples)2966 void handleQuarters(XPathParts parts, String value, List<String> examples) { 2967 String calendarId = parts.getAttributeValue(3, "type"); 2968 if (!calendarId.equals("gregorian")) { 2969 return; 2970 } 2971 String width = parts.findAttributeValue("quarterWidth", "type"); 2972 if (width.equals("narrow")) { 2973 return; 2974 } 2975 String context = parts.findAttributeValue("quarterContext", "type"); 2976 String type = parts.getAttributeValue(-1, "type"); // 1-indexed 2977 int quarterIndex = Integer.parseInt(type) - 1; 2978 String skeleton = (width.equals("wide")) ? "yQQQQ" : "yQQQ"; 2979 String checkPath = 2980 "//ldml/dates/calendars/calendar[@type=\"" 2981 + calendarId 2982 + "\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" 2983 + skeleton 2984 + "\"]"; 2985 String dateFormat = cldrFile.getWinningValue(checkPath); 2986 SimpleDateFormat sdf = icuServiceBuilder.getDateFormat(calendarId, dateFormat); 2987 DateFormatSymbols dfs = sdf.getDateFormatSymbols(); 2988 int widthVal = width.equals("abbreviated") ? 0 : 1; 2989 String[] quarterNames = dfs.getQuarters(0, widthVal); // 0 for formatting 2990 quarterNames[quarterIndex] = value; 2991 dfs.setQuarters(quarterNames, 0, widthVal); 2992 sdf.setDateFormatSymbols(dfs); 2993 sdf.setTimeZone(ZONE_SAMPLE); 2994 final int[] monthSamples = new int[] {1, 4, 7, 10}; // {feb, may, oct, nov} 2995 int month = monthSamples[quarterIndex]; 2996 calendar.set(1999, month, 5, 13, 25, 59); 2997 Date sample = calendar.getTime(); 2998 examples.add(sdf.format(sample)); 2999 } 3000 3001 /* Add relative date/time examples, choosing appropriate 3002 * patterns as needed for relative dates vs relative times. 3003 * Additionally, for relativeTimePattern items, ensure that 3004 * numeric example corresponds to the count represented by the item. 3005 */ handleRelative( String xpath, XPathParts parts, String value, List<String> examples)3006 private void handleRelative( 3007 String xpath, XPathParts parts, String value, List<String> examples) { 3008 String skeleton; 3009 String type = parts.findAttributeValue("field", "type"); 3010 if (type.startsWith("hour")) { 3011 skeleton = "Hm"; 3012 } else if (type.startsWith("minute") || type.startsWith("second")) { 3013 skeleton = "ms"; 3014 } else if (type.startsWith("year") 3015 || type.startsWith("month") 3016 || type.startsWith("quarter")) { 3017 skeleton = "yMMMM"; 3018 } else { 3019 skeleton = "MMMMd"; 3020 } 3021 String checkPath = 3022 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" 3023 + skeleton 3024 + "\"]"; 3025 String dateFormat = cldrFile.getWinningValue(checkPath); 3026 SimpleDateFormat sdf = icuServiceBuilder.getDateFormat("gregorian", dateFormat); 3027 String sampleDate = sdf.format(DATE_SAMPLE); 3028 String example1 = 3029 value.substring(0, 1).toUpperCase() + value.substring(1) + " (" + sampleDate + ")"; 3030 String example2 = sampleDate + " (" + value + ")"; 3031 if (parts.contains("relativeTimePattern")) { // has placeholder 3032 String count = parts.getAttributeValue(-1, "count"); 3033 String exampleCount = COUNTS.get(count); 3034 examples.add(invertBackground(format(setBackground(example1), exampleCount))); 3035 examples.add(invertBackground(format(setBackground(example2), exampleCount))); 3036 } else { 3037 examples.add(format(example1)); 3038 examples.add(format(example2)); 3039 } 3040 } 3041 3042 /** 3043 * @param elementToOverride the element that is to be overridden 3044 * @param element the overriding element 3045 * @param value the value to override element with 3046 * @return 3047 */ getLocaleDisplayPattern(String elementToOverride, String element, String value)3048 private String getLocaleDisplayPattern(String elementToOverride, String element, String value) { 3049 final String localeDisplayPatternPath = "//ldml/localeDisplayNames/localeDisplayPattern/"; 3050 if (elementToOverride.equals(element)) { 3051 return value; 3052 } else { 3053 return cldrFile.getWinningValue(localeDisplayPatternPath + elementToOverride); 3054 } 3055 } 3056 handleDisplayNames( String xpath, XPathParts parts, String value, List<String> examples)3057 private void handleDisplayNames( 3058 String xpath, XPathParts parts, String value, List<String> examples) { 3059 String result = null; 3060 if (parts.contains("codePatterns")) { 3061 // ldml/localeDisplayNames/codePatterns/codePattern[@type="language"] 3062 // ldml/localeDisplayNames/codePatterns/codePattern[@type="script"] 3063 // ldml/localeDisplayNames/codePatterns/codePattern[@type="territory"] 3064 String type = parts.getAttributeValue(-1, "type"); 3065 result = 3066 format( 3067 value, 3068 setBackground( 3069 type.equals("language") 3070 ? "ace" 3071 : type.equals("script") 3072 ? "Avst" 3073 : type.equals("territory") ? "057" : "CODE")); 3074 examples.add(result); 3075 return; 3076 } else if (parts.contains("localeDisplayPattern")) { 3077 // ldml/localeDisplayNames/localeDisplayPattern/localePattern 3078 // ldml/localeDisplayNames/localeDisplayPattern/localeSeparator 3079 // ldml/localeDisplayNames/localeDisplayPattern/localeKeyTypePattern 3080 String element = parts.getElement(-1); 3081 value = setBackground(value); 3082 String localeKeyTypePattern = 3083 getLocaleDisplayPattern("localeKeyTypePattern", element, value); 3084 String localePattern = getLocaleDisplayPattern("localePattern", element, value); 3085 String localeSeparator = getLocaleDisplayPattern("localeSeparator", element, value); 3086 3087 List<String> locales = new ArrayList<>(); 3088 if (element.equals("localePattern")) { 3089 locales.add("uz-AF"); 3090 } 3091 locales.add( 3092 element.equals("localeKeyTypePattern") ? "uz-Arab-u-tz-etadd" : "uz-Arab-AF"); 3093 locales.add("uz-Arab-AF-u-tz-etadd-nu-arab"); 3094 // String[] examples = new String[locales.size()]; 3095 for (int i = 0; i < locales.size(); i++) { 3096 examples.add( 3097 invertBackground( 3098 cldrFile.getName( 3099 locales.get(i), 3100 false, 3101 localeKeyTypePattern, 3102 localePattern, 3103 localeSeparator))); 3104 } 3105 return; 3106 } else if (parts.contains("languages") 3107 || parts.contains("scripts") 3108 || parts.contains("territories")) { 3109 // ldml/localeDisplayNames/languages/language[@type="ar"] 3110 // ldml/localeDisplayNames/scripts/script[@type="Arab"] 3111 // ldml/localeDisplayNames/territories/territory[@type="CA"] 3112 String type = parts.getAttributeValue(-1, "type"); 3113 if (type.contains("_")) { 3114 if (value != null && !value.equals(type)) { 3115 result = value; // trivial -- is this beneficial? 3116 } else { 3117 result = cldrFile.getBaileyValue(xpath, null, null); 3118 } 3119 examples.add(result); 3120 return; 3121 } else { 3122 value = setBackground(value); 3123 String nameType = parts.getElement(3); 3124 3125 Map<String, String> likely = supplementalDataInfo.getLikelySubtags(); 3126 String alt = parts.getAttributeValue(-1, "alt"); 3127 boolean isStandAloneValue = "stand-alone".equals(alt); 3128 if (!isStandAloneValue) { 3129 // only do this if the value is not a stand-alone form 3130 String tag = "language".equals(nameType) ? type : "und_" + type; 3131 String max = LikelySubtags.maximize(tag, likely); 3132 if (max == null) { 3133 return; 3134 } 3135 LanguageTagParser ltp = new LanguageTagParser().set(max); 3136 String languageName = null; 3137 String scriptName = null; 3138 String territoryName = null; 3139 if (nameType.equals("language")) { 3140 languageName = value; 3141 } else if (nameType.equals("script")) { 3142 scriptName = value; 3143 } else { 3144 territoryName = value; 3145 } 3146 if (languageName == null) { 3147 languageName = 3148 cldrFile.getStringValueWithBailey( 3149 CLDRFile.getKey(CLDRFile.LANGUAGE_NAME, ltp.getLanguage())); 3150 if (languageName == null) { 3151 languageName = 3152 cldrFile.getStringValueWithBailey( 3153 CLDRFile.getKey(CLDRFile.LANGUAGE_NAME, "en")); 3154 } 3155 if (languageName == null) { 3156 languageName = ltp.getLanguage(); 3157 } 3158 } 3159 if (scriptName == null) { 3160 scriptName = 3161 cldrFile.getStringValueWithBailey( 3162 CLDRFile.getKey(CLDRFile.SCRIPT_NAME, ltp.getScript())); 3163 if (scriptName == null) { 3164 scriptName = 3165 cldrFile.getStringValueWithBailey( 3166 CLDRFile.getKey(CLDRFile.SCRIPT_NAME, "Latn")); 3167 } 3168 if (scriptName == null) { 3169 scriptName = ltp.getScript(); 3170 } 3171 } 3172 if (territoryName == null) { 3173 territoryName = 3174 cldrFile.getStringValueWithBailey( 3175 CLDRFile.getKey(CLDRFile.TERRITORY_NAME, ltp.getRegion())); 3176 if (territoryName == null) { 3177 territoryName = 3178 cldrFile.getStringValueWithBailey( 3179 CLDRFile.getKey(CLDRFile.TERRITORY_NAME, "US")); 3180 } 3181 if (territoryName == null) { 3182 territoryName = ltp.getRegion(); 3183 } 3184 } 3185 languageName = 3186 languageName 3187 .replace('(', '[') 3188 .replace(')', ']') 3189 .replace('(', '[') 3190 .replace(')', ']'); 3191 scriptName = 3192 scriptName 3193 .replace('(', '[') 3194 .replace(')', ']') 3195 .replace('(', '[') 3196 .replace(')', ']'); 3197 territoryName = 3198 territoryName 3199 .replace('(', '[') 3200 .replace(')', ']') 3201 .replace('(', '[') 3202 .replace(')', ']'); 3203 3204 String localePattern = 3205 cldrFile.getStringValueWithBailey( 3206 "//ldml/localeDisplayNames/localeDisplayPattern/localePattern"); 3207 String localeSeparator = 3208 cldrFile.getStringValueWithBailey( 3209 "//ldml/localeDisplayNames/localeDisplayPattern/localeSeparator"); 3210 String scriptTerritory = format(localeSeparator, scriptName, territoryName); 3211 if (!nameType.equals("script")) { 3212 examples.add( 3213 invertBackground( 3214 format(localePattern, languageName, territoryName))); 3215 } 3216 if (!nameType.equals("territory")) { 3217 examples.add( 3218 invertBackground(format(localePattern, languageName, scriptName))); 3219 } 3220 examples.add( 3221 invertBackground(format(localePattern, languageName, scriptTerritory))); 3222 } 3223 Output<String> pathWhereFound; 3224 if (isStandAloneValue 3225 || cldrFile.getStringValueWithBailey( 3226 xpath + ALT_STAND_ALONE, 3227 pathWhereFound = new Output<>(), 3228 null) 3229 == null 3230 || !pathWhereFound.value.contains(ALT_STAND_ALONE)) { 3231 // only do this if either it is a stand-alone form, 3232 // or it isn't and there is no separate stand-alone form 3233 // the extra check after the == null is to make sure that we don't have sideways 3234 // inheritance 3235 String codePattern = 3236 cldrFile.getStringValueWithBailey( 3237 "//ldml/localeDisplayNames/codePatterns/codePattern[@type=\"" 3238 + nameType 3239 + "\"]"); 3240 examples.add(invertBackground(format(codePattern, value))); 3241 } 3242 return; 3243 } 3244 } 3245 } 3246 formatExampleList(String[] examples)3247 private String formatExampleList(String[] examples) { 3248 String result = examples[0]; 3249 for (int i = 1, len = examples.length; i < len; i++) { 3250 result = addExampleResult(examples[i], result); 3251 } 3252 return result; 3253 } 3254 3255 /** 3256 * Return examples formatted as string, with null returned for null or empty examples. 3257 * 3258 * @param examples 3259 * @return 3260 */ formatExampleList(Collection<String> examples)3261 public String formatExampleList(Collection<String> examples) { 3262 if (examples == null || examples.isEmpty()) { 3263 return null; 3264 } 3265 String result = ""; 3266 boolean first = true; 3267 for (String example : examples) { 3268 if (first) { 3269 result = example; 3270 first = false; 3271 } else { 3272 result = addExampleResult(example, result); 3273 } 3274 } 3275 return result; 3276 } 3277 format(String format, Object... objects)3278 public static String format(String format, Object... objects) { 3279 if (format == null) return null; 3280 return MessageFormat.format(format, objects); 3281 } 3282 unchainException(Exception e)3283 public static String unchainException(Exception e) { 3284 String stackStr = "[unknown stack]<br>"; 3285 try { 3286 StringWriter asString = new StringWriter(); 3287 e.printStackTrace(new PrintWriter(asString)); 3288 stackStr = "<pre>" + asString + "</pre>"; 3289 } catch (Throwable tt) { 3290 // ... 3291 } 3292 return stackStr; 3293 } 3294 3295 /** 3296 * Put a background on an item, skipping enclosed patterns. 3297 * 3298 * @param inputPattern 3299 * @return 3300 */ setBackground(String inputPattern)3301 private String setBackground(String inputPattern) { 3302 if (inputPattern == null) { 3303 return "?"; 3304 } 3305 Matcher m = PARAMETER.matcher(inputPattern); 3306 return backgroundStartSymbol 3307 + m.replaceAll(backgroundEndSymbol + "$1" + backgroundStartSymbol) 3308 + backgroundEndSymbol; 3309 } 3310 3311 /** 3312 * Put a background on an item, skipping enclosed patterns, except for {0} 3313 * 3314 * @param input 3315 * @param patternToEmbed 3316 * @return 3317 */ setBackgroundExceptMatch(String input, Pattern patternToEmbed)3318 private String setBackgroundExceptMatch(String input, Pattern patternToEmbed) { 3319 Matcher m = patternToEmbed.matcher(input); 3320 return backgroundStartSymbol 3321 + m.replaceAll(backgroundEndSymbol + "$1" + backgroundStartSymbol) 3322 + backgroundEndSymbol; 3323 } 3324 3325 /** 3326 * Put a background on an item, skipping enclosed patterns, except for {0} 3327 * 3328 * @param inputPattern 3329 * @param patternToEmbed 3330 * @return 3331 */ setBackgroundOnMatch(String inputPattern, Pattern patternToEmbed)3332 private String setBackgroundOnMatch(String inputPattern, Pattern patternToEmbed) { 3333 Matcher m = patternToEmbed.matcher(inputPattern); 3334 return m.replaceAll(backgroundStartSymbol + "$1" + backgroundEndSymbol); 3335 } 3336 3337 /** 3338 * This is called just before we return a result. It fixes the special characters that were 3339 * added by setBackground. 3340 * 3341 * @param input string with special characters from setBackground. 3342 * @return string with HTML for the background. 3343 */ finalizeBackground(String input)3344 private String finalizeBackground(String input) { 3345 if (input == null) { 3346 return null; 3347 } 3348 String coreString = 3349 TransliteratorUtilities.toHTML 3350 .transliterate(input) 3351 .replace(backgroundStartSymbol + backgroundEndSymbol, "") 3352 // remove null runs 3353 .replace(backgroundEndSymbol + backgroundStartSymbol, "") 3354 // remove null runs 3355 .replace(backgroundStartSymbol, backgroundStart) 3356 .replace(backgroundEndSymbol, backgroundEnd) 3357 .replace(backgroundAutoStartSymbol, backgroundAutoStart) 3358 .replace(backgroundAutoEndSymbol, backgroundAutoEnd) 3359 .replace(exampleSeparatorSymbol, exampleEnd + exampleStart) 3360 .replace(exampleStartAutoSymbol, exampleStartAuto) 3361 .replace(exampleStartRTLSymbol, exampleStartRTL) 3362 .replace(exampleStartHeaderSymbol, exampleStartHeader) 3363 .replace(exampleEndSymbol, exampleEnd) 3364 .replace(startItalicSymbol, startItalic) 3365 .replace(endItalicSymbol, endItalic) 3366 .replace(startSupSymbol, startSup) 3367 .replace(endSupSymbol, endSup); 3368 // If we are not showing context, we use exampleSeparatorSymbol between examples, 3369 // and then need to add the initial exampleStart and final exampleEnd. 3370 return (input.contains(exampleStartAutoSymbol)) 3371 ? coreString 3372 : exampleStart + coreString + exampleEnd; 3373 } 3374 invertBackground(String input)3375 private String invertBackground(String input) { 3376 return input == null 3377 ? null 3378 : backgroundStartSymbol 3379 + input.replace(backgroundStartSymbol, backgroundTempSymbol) 3380 .replace(backgroundEndSymbol, backgroundStartSymbol) 3381 .replace(backgroundTempSymbol, backgroundEndSymbol) 3382 + backgroundEndSymbol; 3383 } 3384 removeEmptyRuns(String input)3385 private String removeEmptyRuns(String input) { 3386 return input.replace(backgroundStartSymbol + backgroundEndSymbol, "") 3387 .replace(backgroundEndSymbol + backgroundStartSymbol, ""); 3388 } 3389 3390 /** 3391 * Utility to format using a gmtHourString, gmtFormat, and an integer hours. We only need the 3392 * hours because that's all the TZDB IDs need. Should merge this eventually into 3393 * TimeZoneFormatter and call there. 3394 * 3395 * @param gmtHourString 3396 * @param gmtFormat 3397 * @param hours 3398 * @return 3399 */ getGMTFormat(String gmtHourString, String gmtFormat, int hours)3400 private String getGMTFormat(String gmtHourString, String gmtFormat, int hours) { 3401 return getGMTFormat(gmtHourString, gmtFormat, hours, 0); 3402 } 3403 getGMTFormat(String gmtHourString, String gmtFormat, int hours, int minutes)3404 private String getGMTFormat(String gmtHourString, String gmtFormat, int hours, int minutes) { 3405 boolean hoursBackground = false; 3406 if (gmtHourString == null) { 3407 hoursBackground = true; 3408 gmtHourString = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/hourFormat"); 3409 } 3410 if (gmtFormat == null) { 3411 hoursBackground = false; // for the hours case 3412 gmtFormat = 3413 setBackground(cldrFile.getWinningValue("//ldml/dates/timeZoneNames/gmtFormat")); 3414 } 3415 String[] plusMinus = gmtHourString.split(";"); 3416 3417 SimpleDateFormat dateFormat = 3418 icuServiceBuilder.getDateFormat("gregorian", plusMinus[hours >= 0 ? 0 : 1]); 3419 dateFormat.setTimeZone(ZONE_SAMPLE); 3420 calendar.set(1999, 9, 27, Math.abs(hours), minutes, 0); // 1999-09-13 13:25:59 3421 Date sample = calendar.getTime(); 3422 String hourString = dateFormat.format(sample); 3423 if (hoursBackground) { 3424 hourString = setBackground(hourString); 3425 } 3426 String result = format(gmtFormat, hourString); 3427 return result; 3428 } 3429 getMZTimeFormat()3430 private String getMZTimeFormat() { 3431 String timeFormat = 3432 cldrFile.getWinningValue( 3433 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/timeFormats/timeFormatLength[@type=\"short\"]/timeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]"); 3434 if (timeFormat == null) { 3435 timeFormat = "HH:mm"; 3436 } 3437 // the following is <= because the TZDB inverts the hours 3438 SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian", timeFormat); 3439 dateFormat.setTimeZone(ZONE_SAMPLE); 3440 calendar.set(1999, 9, 13, 13, 25, 59); // 1999-09-13 13:25:59 3441 Date sample = calendar.getTime(); 3442 String result = dateFormat.format(sample); 3443 return result; 3444 } 3445 3446 /** 3447 * Return a help string, in html, that should be shown in the Zoomed view. Presumably at the end 3448 * of each help section is something like: <br> 3449 * <br>For more information, see <a 3450 * href='http://unicode.org/cldr/wiki?SurveyToolHelp/characters'>help</a>. <br> 3451 * The result is valid HTML. Set listPlaceholders to true to include a HTML-formatted table of 3452 * all placeholders required in the value.<br> 3453 * TODO: add more help, and modify to get from property or xml file for easy modification. 3454 * 3455 * @return null if none available. 3456 */ getHelpHtml(String xpath, String value)3457 public synchronized String getHelpHtml(String xpath, String value) { 3458 // lazy initialization 3459 if (pathDescription == null) { 3460 Map<String, List<Set<String>>> starredPaths = new HashMap<>(); 3461 Map<String, String> extras = new HashMap<>(); 3462 3463 this.pathDescription = 3464 new PathDescription( 3465 supplementalDataInfo, 3466 englishFile, 3467 extras, 3468 starredPaths, 3469 PathDescription.ErrorHandling.CONTINUE); 3470 3471 if (helpMessages == null) { 3472 helpMessages = new HelpMessages("test_help_messages.html"); 3473 } 3474 } 3475 3476 // now get the description 3477 3478 Level level = CONFIG.getCoverageInfo().getCoverageLevel(xpath, cldrFile.getLocaleID()); 3479 String description = pathDescription.getDescription(xpath, value, null); 3480 if (description == null || description.equals("SKIP")) { 3481 return null; 3482 } 3483 int start = 0; 3484 StringBuilder buffer = new StringBuilder(); 3485 3486 Matcher URLMatcher = URL_PATTERN.matcher(""); 3487 while (URLMatcher.reset(description).find(start)) { 3488 final String url = URLMatcher.group(); 3489 buffer.append( 3490 TransliteratorUtilities.toHTML.transliterate( 3491 description.substring(start, URLMatcher.start()))) 3492 .append("<a target='CLDR-ST-DOCS' href='") 3493 .append(url) 3494 .append("'>") 3495 .append(url) 3496 .append("</a>"); 3497 start = URLMatcher.end(); 3498 } 3499 buffer.append(TransliteratorUtilities.toHTML.transliterate(description.substring(start))); 3500 if (AnnotationUtil.pathIsAnnotation(xpath)) { 3501 XPathParts emoji = XPathParts.getFrozenInstance(xpath); 3502 String cp = emoji.getAttributeValue(-1, "cp"); 3503 String minimal = Utility.hex(cp).replace(',', '_').toLowerCase(Locale.ROOT); 3504 buffer.append( 3505 "<br><img height='64px' width='auto' src='images/emoji/emoji_" 3506 + minimal 3507 + ".png'>"); 3508 } 3509 return buffer.toString(); 3510 } 3511 simplify(String exampleHtml)3512 public static String simplify(String exampleHtml) { 3513 return simplify(exampleHtml, false); 3514 } 3515 simplify(String exampleHtml, boolean internal)3516 public static String simplify(String exampleHtml, boolean internal) { 3517 if (exampleHtml == null) { 3518 return null; 3519 } 3520 if (internal) { 3521 return "〖" 3522 + exampleHtml 3523 .replace(backgroundStartSymbol, "❬") 3524 .replace(backgroundEndSymbol, "❭") 3525 + "〗"; 3526 } 3527 int startIndex = exampleHtml.indexOf(exampleStartHeader); 3528 if (startIndex >= 0) { 3529 int endIndex = exampleHtml.indexOf(exampleEnd, startIndex); 3530 if (endIndex > startIndex) { 3531 // remove header for context examples 3532 endIndex += exampleEnd.length(); 3533 String head = exampleHtml.substring(0, startIndex); 3534 String tail = exampleHtml.substring(endIndex); 3535 exampleHtml = head + tail; 3536 } 3537 } 3538 return exampleHtml 3539 .replace("<div class='cldr_example'>", "〖") 3540 .replace("<div class='cldr_example_auto' dir='auto'>", "【") 3541 .replace("<div class='cldr_example_rtl' dir='rtl'>", "【⃪") 3542 .replace("</div>", "〗") 3543 .replace("<span class='cldr_substituted'>", "❬") 3544 .replace("</span>", "❭"); 3545 } 3546 } 3547