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