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