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