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