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