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