xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/test/ExampleGenerator.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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      * &lt;br&gt;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