xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/ICUServiceBuilder.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.util;
2 
3 import com.ibm.icu.text.DateFormat;
4 import com.ibm.icu.text.DateFormatSymbols;
5 import com.ibm.icu.text.DecimalFormat;
6 import com.ibm.icu.text.DecimalFormatSymbols;
7 import com.ibm.icu.text.MessageFormat;
8 import com.ibm.icu.text.NumberFormat;
9 import com.ibm.icu.text.RuleBasedCollator;
10 import com.ibm.icu.text.SimpleDateFormat;
11 import com.ibm.icu.text.UTF16;
12 import com.ibm.icu.text.UnicodeSet;
13 import com.ibm.icu.util.Calendar;
14 import com.ibm.icu.util.Currency;
15 import com.ibm.icu.util.Output;
16 import com.ibm.icu.util.TimeZone;
17 import com.ibm.icu.util.ULocale;
18 import java.text.ParseException;
19 import java.util.ArrayList;
20 import java.util.Date;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Objects;
25 import org.unicode.cldr.util.CLDRFile.Status;
26 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
27 import org.unicode.cldr.util.SupplementalDataInfo.CurrencyNumberInfo;
28 
29 public class ICUServiceBuilder {
30     public static Currency NO_CURRENCY = Currency.getInstance("XXX");
31     private CLDRFile cldrFile;
32     private CLDRFile collationFile;
33     private static final Map<CLDRLocale, ICUServiceBuilder> ISBMap = new HashMap<>();
34 
35     private static final TimeZone utc = TimeZone.getTimeZone("GMT");
36     private static final DateFormat iso =
37             new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", ULocale.ENGLISH);
38 
39     static {
40         iso.setTimeZone(utc);
41     }
42 
isoDateFormat(Date date)43     public static String isoDateFormat(Date date) {
44         return iso.format(date);
45     }
46 
isoDateFormat(long value)47     public static String isoDateFormat(long value) {
48         return iso.format(new Date(value));
49     }
50 
isoDateParse(String date)51     public static Date isoDateParse(String date) throws ParseException {
52         return iso.parse(date);
53     }
54 
55     private final Map<String, SimpleDateFormat> cacheDateFormats = new HashMap<>();
56     private final Map<String, DateFormatSymbols> cacheDateFormatSymbols = new HashMap<>();
57     private final Map<String, NumberFormat> cacheNumberFormats = new HashMap<>();
58     private final Map<String, DecimalFormatSymbols> cacheDecimalFormatSymbols = new HashMap<>();
59     private final Map<String, RuleBasedCollator> cacheRuleBasedCollators = new HashMap<>();
60 
61     /**
62      * Caching can be disabled for some ICUServiceBuilder instances while still enabled for others.
63      */
64     private boolean cachingIsEnabled = true;
65 
setCachingEnabled(boolean enabled)66     public void setCachingEnabled(boolean enabled) {
67         cachingIsEnabled = enabled;
68     }
69 
70     /**
71      * TODO: the ability to clear the cache(s) is temporarily useful for debugging, and may or may
72      * not be useful in the long run. In the meantime, this should be false except while debugging.
73      * Reference: <a href="https://unicode-org.atlassian.net/browse/CLDR-13970">CLDR-13970</a>
74      */
75     public static final boolean ISB_CAN_CLEAR_CACHE = true;
76 
clearCache()77     public void clearCache() {
78         if (ISB_CAN_CLEAR_CACHE) {
79             cacheDateFormats.clear();
80             cacheDateFormatSymbols.clear();
81             cacheNumberFormats.clear();
82             cacheDecimalFormatSymbols.clear();
83             cacheRuleBasedCollators.clear();
84         }
85     }
86 
87     private SupplementalDataInfo supplementalData;
88 
89     private static final int[] DateFormatValues = {
90         -1, DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG, DateFormat.FULL
91     };
92     private static final String[] DateFormatNames = {"none", "short", "medium", "long", "full"};
93 
94     private static final String[] Days = {"sun", "mon", "tue", "wed", "thu", "fri", "sat"};
95 
getCldrFile()96     public CLDRFile getCldrFile() {
97         return cldrFile;
98     }
99 
setCldrFile(CLDRFile cldrFile)100     public ICUServiceBuilder setCldrFile(CLDRFile cldrFile) {
101         if (!cldrFile.isResolved()) throw new IllegalArgumentException("CLDRFile must be resolved");
102         this.cldrFile = cldrFile;
103         supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo();
104         cacheDateFormats.clear();
105         cacheNumberFormats.clear();
106         cacheDateFormatSymbols.clear();
107         cacheDecimalFormatSymbols.clear();
108         cacheRuleBasedCollators.clear();
109         return this;
110     }
111 
forLocale(CLDRLocale locale)112     public static ICUServiceBuilder forLocale(CLDRLocale locale) {
113 
114         ICUServiceBuilder result = ISBMap.get(locale);
115 
116         if (result == null) {
117             result = new ICUServiceBuilder();
118 
119             if (locale != null) {
120                 // CAUTION: this fails for files in seed, when called for DAIP, for CLDRModify,
121                 // since CLDRPaths.MAIN_DIRECTORY is "common/main" NOT "seed/main".
122                 // Fortunately CLDR no longer uses the "seed" directory -- as of 2023 it is empty
123                 // except for README files. If CLDR ever uses "seed" again, however, this will
124                 // become a problem again.
125                 result.cldrFile =
126                         Factory.make(CLDRPaths.MAIN_DIRECTORY, ".*")
127                                 .make(locale.getBaseName(), true);
128                 result.collationFile =
129                         Factory.make(CLDRPaths.COLLATION_DIRECTORY, ".*")
130                                 .makeWithFallback(locale.getBaseName());
131             }
132             result.supplementalData =
133                     SupplementalDataInfo.getInstance(CLDRPaths.DEFAULT_SUPPLEMENTAL_DIRECTORY);
134             result.cacheDateFormats.clear();
135             result.cacheNumberFormats.clear();
136             result.cacheDateFormatSymbols.clear();
137             result.cacheDecimalFormatSymbols.clear();
138             result.cacheRuleBasedCollators.clear();
139 
140             ISBMap.put(locale, result);
141         }
142         return result;
143     }
144 
getRuleBasedCollator(String type)145     public RuleBasedCollator getRuleBasedCollator(String type) throws Exception {
146         RuleBasedCollator col = cachingIsEnabled ? cacheRuleBasedCollators.get(type) : null;
147         if (col == null) {
148             col = _getRuleBasedCollator(type);
149             if (cachingIsEnabled) {
150                 cacheRuleBasedCollators.put(type, col);
151             }
152         }
153         return (RuleBasedCollator) col.clone();
154     }
155 
_getRuleBasedCollator(String type)156     private RuleBasedCollator _getRuleBasedCollator(String type) throws Exception {
157         String rules = "";
158         String collationType;
159         if ("default".equals(type)) {
160             String path = "//ldml/collations/defaultCollation";
161             collationType = collationFile.getWinningValueWithBailey(path);
162         } else {
163             collationType = type;
164         }
165         String path = "";
166         String importPath =
167                 "//ldml/collations/collation[@visibility=\"external\"][@type=\""
168                         + collationType
169                         + "\"]/import[@type=\"standard\"]";
170         if (collationFile.isHere(importPath)) {
171             String fullPath = collationFile.getFullXPath(importPath);
172             XPathParts xpp = XPathParts.getFrozenInstance(fullPath);
173             String importSource = xpp.getAttributeValue(-1, "source");
174             String importType = xpp.getAttributeValue(-1, "type");
175             CLDRLocale importLocale = CLDRLocale.getInstance(importSource);
176             CLDRFile importCollationFile =
177                     Factory.make(CLDRPaths.COLLATION_DIRECTORY, ".*")
178                             .makeWithFallback(importLocale.getBaseName());
179             path = "//ldml/collations/collation[@type=\"" + importType + "\"]/cr";
180             rules = importCollationFile.getStringValue(path);
181 
182         } else {
183             path = "//ldml/collations/collation[@type=\"" + collationType + "\"]/cr";
184             rules = collationFile.getStringValue(path);
185         }
186         RuleBasedCollator col;
187         if (rules != null && rules.length() > 0) col = new RuleBasedCollator(rules);
188         else col = (RuleBasedCollator) RuleBasedCollator.getInstance();
189 
190         return col;
191     }
192 
getRuleBasedCollator()193     public RuleBasedCollator getRuleBasedCollator() throws Exception {
194         return getRuleBasedCollator("default");
195     }
196 
getDateFormat(String calendar, int dateIndex, int timeIndex)197     public SimpleDateFormat getDateFormat(String calendar, int dateIndex, int timeIndex) {
198         return getDateFormat(calendar, dateIndex, timeIndex, null);
199     }
200 
getDateFormat( String calendar, int dateIndex, int timeIndex, String numbersOverride)201     public SimpleDateFormat getDateFormat(
202             String calendar, int dateIndex, int timeIndex, String numbersOverride) {
203         String key = cldrFile.getLocaleID() + "," + calendar + "," + dateIndex + "," + timeIndex;
204         SimpleDateFormat result = cachingIsEnabled ? cacheDateFormats.get(key) : null;
205         if (result != null) return (SimpleDateFormat) result.clone();
206 
207         String pattern = getPattern(calendar, dateIndex, timeIndex);
208 
209         result = getFullFormat(calendar, pattern, numbersOverride);
210         if (cachingIsEnabled) {
211             cacheDateFormats.put(key, result);
212         }
213         // System.out.println("created " + key);
214         return (SimpleDateFormat) result.clone();
215     }
216 
getDateFormat(String calendar, String pattern, String numbersOverride)217     public SimpleDateFormat getDateFormat(String calendar, String pattern, String numbersOverride) {
218         String key =
219                 cldrFile.getLocaleID() + "," + calendar + ",," + pattern + ",,," + numbersOverride;
220         SimpleDateFormat result = cachingIsEnabled ? cacheDateFormats.get(key) : null;
221         if (result != null) return (SimpleDateFormat) result.clone();
222         result = getFullFormat(calendar, pattern, numbersOverride);
223         if (cachingIsEnabled) {
224             cacheDateFormats.put(key, result);
225         }
226         // System.out.println("created " + key);
227         return (SimpleDateFormat) result.clone();
228     }
229 
getDateFormat(String calendar, String pattern)230     public SimpleDateFormat getDateFormat(String calendar, String pattern) {
231         return getDateFormat(calendar, pattern, null);
232     }
233 
getFullFormat( String calendar, String pattern, String numbersOverride)234     private SimpleDateFormat getFullFormat(
235             String calendar, String pattern, String numbersOverride) {
236         ULocale curLocaleWithCalendar =
237                 new ULocale(cldrFile.getLocaleID() + "@calendar=" + calendar);
238         SimpleDateFormat result =
239                 new SimpleDateFormat(pattern, numbersOverride, curLocaleWithCalendar); // formatData
240         // TODO Serious Hack, until ICU #4915 is fixed. => It *was* fixed in ICU 3.8, so now use
241         // current locale.(?)
242         Calendar cal = Calendar.getInstance(curLocaleWithCalendar);
243         // TODO look these up and set them
244         // cal.setFirstDayOfWeek()
245         // cal.setMinimalDaysInFirstWeek()
246         cal.setTimeZone(utc);
247         result.setCalendar(cal);
248 
249         result.setDateFormatSymbols((DateFormatSymbols) _getDateFormatSymbols(calendar).clone());
250 
251         // formatData.setZoneStrings();
252 
253         NumberFormat numberFormat = result.getNumberFormat();
254         if (numberFormat instanceof DecimalFormat) {
255             DecimalFormat df = (DecimalFormat) numberFormat;
256             df.setGroupingUsed(false);
257             df.setDecimalSeparatorAlwaysShown(false);
258             df.setParseIntegerOnly(true); /* So that dd.MM.yy can be parsed */
259             df.setMinimumFractionDigits(0); // To prevent "Jan 1.00, 1997.00"
260         }
261         result.setNumberFormat((NumberFormat) numberFormat.clone());
262         // Need to put the field specific number format override formatters back in place, since
263         // the previous result.setNumberFormat above nukes them.
264         if (numbersOverride != null && numbersOverride.contains("=")) {
265             String[] overrides = numbersOverride.split(",");
266             for (String override : overrides) {
267                 String[] fields = override.split("=", 2);
268                 if (fields.length == 2) {
269                     String overrideField = fields[0].substring(0, 1);
270                     ULocale curLocaleWithNumbers =
271                             new ULocale(cldrFile.getLocaleID() + "@numbers=" + fields[1]);
272                     NumberFormat onf =
273                             NumberFormat.getInstance(
274                                     curLocaleWithNumbers, NumberFormat.NUMBERSTYLE);
275                     if (onf instanceof DecimalFormat) {
276                         DecimalFormat df = (DecimalFormat) onf;
277                         df.setGroupingUsed(false);
278                         df.setDecimalSeparatorAlwaysShown(false);
279                         df.setParseIntegerOnly(true); /* So that dd.MM.yy can be parsed */
280                         df.setMinimumFractionDigits(0); // To prevent "Jan 1.00, 1997.00"
281                     }
282                     result.setNumberFormat(overrideField, onf);
283                 }
284             }
285         }
286         return result;
287     }
288 
_getDateFormatSymbols(String calendar)289     private DateFormatSymbols _getDateFormatSymbols(String calendar) {
290         String key = cldrFile.getLocaleID() + "," + calendar;
291         DateFormatSymbols result = cachingIsEnabled ? cacheDateFormatSymbols.get(key) : null;
292         if (result != null) return (DateFormatSymbols) result.clone();
293 
294         String[] last;
295         // TODO We would also like to be able to set the new symbols leapMonthPatterns &
296         // shortYearNames
297         // (related to Chinese calendar) to their currently-winning values. Until we have the
298         // necessary
299         // setters (per ICU ticket #9385) we can't do that. However, we can at least use the values
300         // that ICU has for the current locale, instead of using the values that ICU has for root.
301         ULocale curLocaleWithCalendar =
302                 new ULocale(cldrFile.getLocaleID() + "@calendar=" + calendar);
303         DateFormatSymbols formatData = new DateFormatSymbols(curLocaleWithCalendar);
304 
305         String prefix = "//ldml/dates/calendars/calendar[@type=\"" + calendar + "\"]/";
306 
307         formatData.setAmPmStrings(
308                 last =
309                         getArrayOfWinningValues(
310                                 new String[] {
311                                     getDayPeriods(prefix, "format", "wide", "am"),
312                                     getDayPeriods(prefix, "format", "wide", "pm")
313                                 }));
314         checkFound(last);
315 
316         int minEras = (calendar.equals("chinese") || calendar.equals("dangi")) ? 0 : 1;
317 
318         List<String> temp = getArray(prefix + "eras/eraAbbr/era[@type=\"", 0, null, "\"]", minEras);
319         formatData.setEras(last = temp.toArray(new String[temp.size()]));
320         if (minEras != 0) checkFound(last);
321 
322         temp = getArray(prefix + "eras/eraNames/era[@type=\"", 0, null, "\"]", minEras);
323         formatData.setEraNames(last = temp.toArray(new String[temp.size()]));
324         if (minEras != 0) checkFound(last);
325 
326         formatData.setMonths(
327                 getArray(prefix, "month", "format", "wide"),
328                 DateFormatSymbols.FORMAT,
329                 DateFormatSymbols.WIDE);
330         formatData.setMonths(
331                 getArray(prefix, "month", "format", "abbreviated"),
332                 DateFormatSymbols.FORMAT,
333                 DateFormatSymbols.ABBREVIATED);
334         formatData.setMonths(
335                 getArray(prefix, "month", "format", "narrow"),
336                 DateFormatSymbols.FORMAT,
337                 DateFormatSymbols.NARROW);
338 
339         formatData.setMonths(
340                 getArray(prefix, "month", "stand-alone", "wide"),
341                 DateFormatSymbols.STANDALONE,
342                 DateFormatSymbols.WIDE);
343         formatData.setMonths(
344                 getArray(prefix, "month", "stand-alone", "abbreviated"),
345                 DateFormatSymbols.STANDALONE,
346                 DateFormatSymbols.ABBREVIATED);
347         formatData.setMonths(
348                 getArray(prefix, "month", "stand-alone", "narrow"),
349                 DateFormatSymbols.STANDALONE,
350                 DateFormatSymbols.NARROW);
351         formatData.setWeekdays(
352                 getArray(prefix, "day", "format", "wide"),
353                 DateFormatSymbols.FORMAT,
354                 DateFormatSymbols.WIDE);
355         formatData.setWeekdays(
356                 getArray(prefix, "day", "format", "abbreviated"),
357                 DateFormatSymbols.FORMAT,
358                 DateFormatSymbols.ABBREVIATED);
359         formatData.setWeekdays(
360                 getArray(prefix, "day", "format", "narrow"),
361                 DateFormatSymbols.FORMAT,
362                 DateFormatSymbols.NARROW);
363 
364         formatData.setWeekdays(
365                 getArray(prefix, "day", "stand-alone", "wide"),
366                 DateFormatSymbols.STANDALONE,
367                 DateFormatSymbols.WIDE);
368         formatData.setWeekdays(
369                 getArray(prefix, "day", "stand-alone", "abbreviated"),
370                 DateFormatSymbols.STANDALONE,
371                 DateFormatSymbols.ABBREVIATED);
372         formatData.setWeekdays(
373                 getArray(prefix, "day", "stand-alone", "narrow"),
374                 DateFormatSymbols.STANDALONE,
375                 DateFormatSymbols.NARROW);
376 
377         // quarters
378 
379         formatData.setQuarters(
380                 getArray(prefix, "quarter", "format", "wide"),
381                 DateFormatSymbols.FORMAT,
382                 DateFormatSymbols.WIDE);
383         formatData.setQuarters(
384                 getArray(prefix, "quarter", "format", "abbreviated"),
385                 DateFormatSymbols.FORMAT,
386                 DateFormatSymbols.ABBREVIATED);
387         formatData.setQuarters(
388                 getArray(prefix, "quarter", "format", "narrow"),
389                 DateFormatSymbols.FORMAT,
390                 DateFormatSymbols.NARROW);
391 
392         formatData.setQuarters(
393                 getArray(prefix, "quarter", "stand-alone", "wide"),
394                 DateFormatSymbols.STANDALONE,
395                 DateFormatSymbols.WIDE);
396         formatData.setQuarters(
397                 getArray(prefix, "quarter", "stand-alone", "abbreviated"),
398                 DateFormatSymbols.STANDALONE,
399                 DateFormatSymbols.ABBREVIATED);
400         formatData.setQuarters(
401                 getArray(prefix, "quarter", "stand-alone", "narrow"),
402                 DateFormatSymbols.STANDALONE,
403                 DateFormatSymbols.NARROW);
404 
405         if (cachingIsEnabled) {
406             cacheDateFormatSymbols.put(key, formatData);
407         }
408         return (DateFormatSymbols) formatData.clone();
409     }
410 
411     /**
412      * Example from en.xml <dayPeriods> <dayPeriodContext type="format"> <dayPeriodWidth
413      * type="wide"> <dayPeriod type="am">AM</dayPeriod> <dayPeriod type="am"
414      * alt="variant">a.m.</dayPeriod> <dayPeriod type="pm">PM</dayPeriod> <dayPeriod type="pm"
415      * alt="variant">p.m.</dayPeriod> </dayPeriodWidth> </dayPeriodContext> </dayPeriods>
416      */
getDayPeriods(String prefix, String context, String width, String type)417     private String getDayPeriods(String prefix, String context, String width, String type) {
418         return prefix
419                 + "dayPeriods/dayPeriodContext[@type=\""
420                 + context
421                 + "\"]/dayPeriodWidth[@type=\""
422                 + width
423                 + "\"]/dayPeriod[@type=\""
424                 + type
425                 + "\"]";
426     }
427 
getArrayOfWinningValues(String[] xpaths)428     private String[] getArrayOfWinningValues(String[] xpaths) {
429         String[] result = new String[xpaths.length];
430         for (int i = 0; i < xpaths.length; i++) {
431             result[i] = cldrFile.getWinningValueWithBailey(xpaths[i]);
432         }
433         checkFound(result, xpaths);
434         return result;
435     }
436 
checkFound(String[] last)437     private void checkFound(String[] last) {
438         if (last == null || last.length == 0 || last[0] == null) {
439             throw new IllegalArgumentException("Failed to load array");
440         }
441     }
442 
checkFound(String[] last, String[] xpaths)443     private void checkFound(String[] last, String[] xpaths) {
444         if (last == null || last.length == 0 || last[0] == null) {
445             throw new IllegalArgumentException("Failed to load array {" + xpaths[0] + ",...}");
446         }
447     }
448 
getPattern(String calendar, int dateIndex, int timeIndex)449     private String getPattern(String calendar, int dateIndex, int timeIndex) {
450         String pattern;
451         if (DateFormatValues[timeIndex] == -1)
452             pattern = getDateTimePattern(calendar, "date", DateFormatNames[dateIndex]);
453         else if (DateFormatValues[dateIndex] == -1)
454             pattern = getDateTimePattern(calendar, "time", DateFormatNames[timeIndex]);
455         else {
456             String p0 = getDateTimePattern(calendar, "time", DateFormatNames[timeIndex]);
457             String p1 = getDateTimePattern(calendar, "date", DateFormatNames[dateIndex]);
458             String datetimePat =
459                     getDateTimePattern(calendar, "dateTime", DateFormatNames[dateIndex]);
460             pattern = MessageFormat.format(datetimePat, (Object[]) new String[] {p0, p1});
461         }
462         return pattern;
463     }
464 
465     /**
466      * @param calendar TODO
467      */
getDateTimePattern(String calendar, String dateOrTime, String type)468     private String getDateTimePattern(String calendar, String dateOrTime, String type) {
469         type = "[@type=\"" + type + "\"]";
470         String key =
471                 "//ldml/dates/calendars/calendar[@type=\""
472                         + calendar
473                         + "\"]/"
474                         + dateOrTime
475                         + "Formats/"
476                         + dateOrTime
477                         + "FormatLength"
478                         + type
479                         + "/"
480                         + dateOrTime
481                         + "Format[@type=\"standard\"]/pattern[@type=\"standard\"]";
482         // change standard to a choice
483 
484         String value = cldrFile.getWinningValueWithBailey(key);
485         if (value == null)
486             throw new IllegalArgumentException(
487                     "locale: "
488                             + cldrFile.getLocaleID()
489                             + "\tpath: "
490                             + key
491                             + CldrUtility.LINE_SEPARATOR
492                             + "value: "
493                             + value);
494         return value;
495     }
496 
497     // enum ArrayType {day, month, quarter};
498 
getArray(String key, String type, String context, String width)499     private String[] getArray(String key, String type, String context, String width) {
500         String prefix =
501                 key
502                         + type
503                         + "s/"
504                         + type
505                         + "Context[@type=\""
506                         + context
507                         + "\"]/"
508                         + type
509                         + "Width[@type=\""
510                         + width
511                         + "\"]/"
512                         + type
513                         + "[@type=\"";
514         String postfix = "\"]";
515         boolean isDay = type.equals("day");
516         final int arrayCount = isDay ? 7 : type.equals("month") ? 12 : 4;
517         List<String> temp =
518                 getArray(prefix, isDay ? 0 : 1, isDay ? Days : null, postfix, arrayCount);
519         if (isDay) temp.add(0, "");
520         String[] result = temp.toArray(new String[temp.size()]);
521         checkFound(result);
522         return result;
523     }
524 
getArray( String prefix, int firstIndex, String[] itemNames, String postfix, int minimumSize)525     private List<String> getArray(
526             String prefix, int firstIndex, String[] itemNames, String postfix, int minimumSize) {
527         List<String> result = new ArrayList<>();
528         String lastType;
529         for (int i = firstIndex; ; ++i) {
530             lastType = itemNames != null && i < itemNames.length ? itemNames[i] : String.valueOf(i);
531             String item = cldrFile.getWinningValueWithBailey(prefix + lastType + postfix);
532             if (item == null) break;
533             result.add(item);
534         }
535         // the following code didn't do anything, so I'm wondering what it was there for?
536         // it's to catch errors
537         if (result.size() < minimumSize) {
538             throw new RuntimeException(
539                     "Internal Error: ICUServiceBuilder.getArray():"
540                             + cldrFile.getLocaleID()
541                             + " "
542                             + prefix
543                             + lastType
544                             + postfix
545                             + " - result.size="
546                             + result.size()
547                             + ", less than acceptable minimum "
548                             + minimumSize);
549         }
550         /*
551          * <months>
552          * <monthContext type="format">
553          * <monthWidth type="abbreviated">
554          * <month type="1">1</month>
555          */
556         return result;
557     }
558 
559     private static String[] NumberNames = {
560         "integer", "decimal", "percent", "scientific"
561     }; // // "standard", , "INR",
562 
563     private static class MyCurrency extends Currency {
564         String symbol;
565         String displayName;
566         int fractDigits;
567         double roundingIncrement;
568 
569         MyCurrency(
570                 String code,
571                 String symbol,
572                 String displayName,
573                 CurrencyNumberInfo currencyNumberInfo) {
574             super(code);
575             this.symbol = symbol == null ? code : symbol;
576             this.displayName = displayName == null ? code : displayName;
577             this.fractDigits = currencyNumberInfo.getDigits();
578             this.roundingIncrement = currencyNumberInfo.getRoundingIncrement();
579         }
580 
581         @Override
582         public String getName(ULocale locale, int nameStyle, boolean[] isChoiceFormat) {
583 
584             String result =
585                     nameStyle == 0
586                             ? this.symbol
587                             : nameStyle == 1
588                                     ? getCurrencyCode()
589                                     : nameStyle == 2 ? displayName : null;
590             if (result == null) throw new IllegalArgumentException();
591             // snagged from currency
592             if (isChoiceFormat != null) {
593                 isChoiceFormat[0] = false;
594             }
595             int i = 0;
596             while (i < result.length() && result.charAt(i) == '=' && i < 2) {
597                 ++i;
598             }
599             if (isChoiceFormat != null) {
600                 isChoiceFormat[0] = (i == 1);
601             }
602             if (i != 0) {
603                 // Skip over first mark
604                 result = result.substring(1);
605             }
606             return result;
607         }
608 
609         /**
610          * Returns the rounding increment for this currency, or 0.0 if no rounding is done by this
611          * currency.
612          *
613          * @return the non-negative rounding increment, or 0.0 if none
614          * @stable ICU 2.2
615          */
616         @Override
617         public double getRoundingIncrement() {
618             return roundingIncrement;
619         }
620 
621         @Override
622         public int getDefaultFractionDigits() {
623             return fractDigits;
624         }
625 
626         @Override
627         public boolean equals(Object other) {
628             if (this == other) {
629                 return true;
630             }
631             if (other == null || !(other instanceof MyCurrency)) {
632                 return false;
633             }
634             MyCurrency that = (MyCurrency) other;
635             return roundingIncrement == that.roundingIncrement
636                     && fractDigits == that.fractDigits
637                     && symbol.equals(that.symbol)
638                     && displayName.equals(that.displayName);
639         }
640 
641         @Override
642         public int hashCode() {
643             return Objects.hash(roundingIncrement, fractDigits, symbol, displayName);
644         }
645     }
646 
647     static int CURRENCY = 0, OTHER_KEY = 1, PATTERN = 2;
648 
649     public DecimalFormat getCurrencyFormat(String currency) {
650         return _getNumberFormat(currency, CURRENCY, null, null);
651     }
652 
653     public DecimalFormat getCurrencyFormat(String currency, String currencySymbol) {
654         return _getNumberFormat(currency, CURRENCY, currencySymbol, null);
655     }
656 
657     public DecimalFormat getCurrencyFormat(
658             String currency, String currencySymbol, String numberSystem) {
659         return _getNumberFormat(currency, CURRENCY, currencySymbol, numberSystem);
660     }
661 
662     public DecimalFormat getNumberFormat(int index) {
663         return _getNumberFormat(NumberNames[index], OTHER_KEY, null, null);
664     }
665 
666     public DecimalFormat getNumberFormat(int index, String numberSystem) {
667         return _getNumberFormat(NumberNames[index], OTHER_KEY, null, numberSystem);
668     }
669 
670     public NumberFormat getGenericNumberFormat(String ns) {
671         NumberFormat result =
672                 cachingIsEnabled
673                         ? cacheNumberFormats.get(cldrFile.getLocaleID() + "@numbers=" + ns)
674                         : null;
675         if (result == null) {
676             ULocale ulocale = new ULocale(cldrFile.getLocaleID() + "@numbers=" + ns);
677             result = NumberFormat.getInstance(ulocale);
678             if (cachingIsEnabled) {
679                 cacheNumberFormats.put(cldrFile.getLocaleID() + "@numbers=" + ns, result);
680             }
681         }
682         return (NumberFormat) result.clone();
683     }
684 
685     public DecimalFormat getNumberFormat(String pattern) {
686         return _getNumberFormat(pattern, PATTERN, null, null);
687     }
688 
689     public DecimalFormat getNumberFormat(String pattern, String numberSystem) {
690         return _getNumberFormat(pattern, PATTERN, null, numberSystem);
691     }
692 
693     private DecimalFormat _getNumberFormat(
694             String key1, int kind, String currencySymbol, String numberSystem) {
695         String localeIDString =
696                 (numberSystem == null)
697                         ? cldrFile.getLocaleID()
698                         : cldrFile.getLocaleID() + "@numbers=" + numberSystem;
699         ULocale ulocale = new ULocale(localeIDString);
700         String key =
701                 (currencySymbol == null)
702                         ? ulocale + "/" + key1 + "/" + kind
703                         : ulocale + "/" + key1 + "/" + kind + "/" + currencySymbol;
704         DecimalFormat result =
705                 cachingIsEnabled ? (DecimalFormat) cacheNumberFormats.get(key) : null;
706         if (result != null) {
707             return (DecimalFormat) result.clone();
708         }
709 
710         String pattern = kind == PATTERN ? key1 : getPattern(key1, kind);
711 
712         DecimalFormatSymbols symbols = _getDecimalFormatSymbols(numberSystem);
713         /*
714          * currencySymbol.equals(other.currencySymbol) &&
715          * intlCurrencySymbol.equals(other.intlCurrencySymbol) &&
716          * padEscape == other.padEscape && // [NEW]
717          * monetarySeparator == other.monetarySeparator);
718          */
719         MyCurrency mc = null;
720         if (kind == CURRENCY) {
721             // in this case numberSystem is null and symbols are for the default system
722             // ^^^^^ NO, that is not true.
723 
724             String prefix = "//ldml/numbers/currencies/currency[@type=\"" + key1 + "\"]/";
725             // /ldml/numbers/currencies/currency[@type="GBP"]/symbol
726             // /ldml/numbers/currencies/currency[@type="GBP"]
727 
728             if (currencySymbol == null) {
729                 currencySymbol = cldrFile.getWinningValueWithBailey(prefix + "symbol");
730             }
731             if (currencySymbol == null) {
732                 throw new NullPointerException(
733                         cldrFile.getSourceLocation(prefix + "symbol")
734                                 + ": "
735                                 + cldrFile.getLocaleID()
736                                 + ": "
737                                 + ": null currencySymbol for "
738                                 + prefix
739                                 + "symbol");
740             }
741             String currencyDecimal = cldrFile.getWinningValueWithBailey(prefix + "decimal");
742             if (currencyDecimal != null) {
743                 (symbols = cloneIfNeeded(symbols))
744                         .setMonetaryDecimalSeparator(currencyDecimal.charAt(0));
745             }
746             String currencyPattern = cldrFile.getWinningValueWithBailey(prefix + "pattern");
747             if (currencyPattern != null) {
748                 pattern = currencyPattern;
749             }
750 
751             String currencyGrouping = cldrFile.getWinningValueWithBailey(prefix + "grouping");
752             if (currencyGrouping != null) {
753                 (symbols = cloneIfNeeded(symbols))
754                         .setMonetaryGroupingSeparator(currencyGrouping.charAt(0));
755             }
756 
757             // <decimal>,</decimal>
758             // <group>.</group>
759 
760             // TODO This is a hack for now, since I am ignoring the possibility of quoted text next
761             // to the symbol
762             if (pattern.contains(";")) { // multi pattern
763                 String[] pieces = pattern.split(";");
764                 for (int i = 0; i < pieces.length; ++i) {
765                     pieces[i] = fixCurrencySpacing(pieces[i], currencySymbol);
766                 }
767                 pattern = org.unicode.cldr.util.CldrUtility.join(pieces, ";");
768             } else {
769                 pattern = fixCurrencySpacing(pattern, currencySymbol);
770             }
771 
772             CurrencyNumberInfo info = supplementalData.getCurrencyNumberInfo(key1);
773 
774             mc =
775                     new MyCurrency(
776                             key1,
777                             currencySymbol,
778                             cldrFile.getWinningValueWithBailey(prefix + "displayName"),
779                             info);
780         }
781         result = new DecimalFormat(pattern, symbols);
782         if (mc != null) {
783             result.setCurrency(mc);
784             result.setMaximumFractionDigits(mc.getDefaultFractionDigits());
785             result.setMinimumFractionDigits(mc.getDefaultFractionDigits());
786         } else {
787             result.setCurrency(NO_CURRENCY);
788         }
789 
790         if (false) {
791             System.out.println(
792                     "creating "
793                             + ulocale
794                             + "\tkey: "
795                             + key
796                             + "\tpattern "
797                             + pattern
798                             + "\tresult: "
799                             + result.toPattern()
800                             + "\t0=>"
801                             + result.format(0));
802             DecimalFormat n2 = (DecimalFormat) NumberFormat.getScientificInstance(ulocale);
803             System.out.println("\tresult: " + n2.toPattern() + "\t0=>" + n2.format(0));
804         }
805         if (kind == OTHER_KEY && key1.equals("integer")) {
806             result.setMaximumFractionDigits(0);
807             result.setDecimalSeparatorAlwaysShown(false);
808             result.setParseIntegerOnly(true);
809         }
810         if (cachingIsEnabled) {
811             cacheNumberFormats.put(key, result);
812         }
813         return (DecimalFormat) result.clone();
814     }
815 
816     private String fixCurrencySpacing(String pattern, String symbol) {
817         int startPos = pattern.indexOf('\u00a4');
818         if (startPos > 0 && beforeCurrencyMatch.contains(UTF16.charAt(symbol, 0))) {
819             int ch = UTF16.charAt(pattern, startPos - 1);
820             if (ch == '#') ch = '0'; // fix pattern
821             if (beforeSurroundingMatch.contains(ch)) {
822                 pattern =
823                         pattern.substring(0, startPos)
824                                 + beforeInsertBetween
825                                 + pattern.substring(startPos);
826             }
827         }
828         int endPos = pattern.lastIndexOf('\u00a4') + 1;
829         if (endPos < pattern.length()
830                 && afterCurrencyMatch.contains(UTF16.charAt(symbol, symbol.length() - 1))) {
831             int ch = UTF16.charAt(pattern, endPos);
832             if (ch == '#') ch = '0'; // fix pattern
833             if (afterSurroundingMatch.contains(ch)) {
834                 pattern =
835                         pattern.substring(0, endPos)
836                                 + afterInsertBetween
837                                 + pattern.substring(endPos);
838             }
839         }
840         return pattern;
841     }
842 
843     private DecimalFormatSymbols cloneIfNeeded(DecimalFormatSymbols symbols) {
844         if (symbols == _getDecimalFormatSymbols(null)) {
845             return (DecimalFormatSymbols) symbols.clone();
846         }
847         return symbols;
848     }
849 
850     public DecimalFormatSymbols getDecimalFormatSymbols(String numberSystem) {
851         return (DecimalFormatSymbols) _getDecimalFormatSymbols(numberSystem).clone();
852     }
853 
854     private DecimalFormatSymbols _getDecimalFormatSymbols(String numberSystem) {
855         String key =
856                 (numberSystem == null)
857                         ? cldrFile.getLocaleID()
858                         : cldrFile.getLocaleID() + "@numbers=" + numberSystem;
859         DecimalFormatSymbols symbols = cachingIsEnabled ? cacheDecimalFormatSymbols.get(key) : null;
860         if (symbols != null) {
861             return (DecimalFormatSymbols) symbols.clone();
862         }
863 
864         symbols = new DecimalFormatSymbols();
865         if (numberSystem == null) {
866             numberSystem =
867                     cldrFile.getWinningValueWithBailey("//ldml/numbers/defaultNumberingSystem");
868         }
869 
870         // currently constants
871         // symbols.setPadEscape(cldrFile.getWinningValueWithBailey("//ldml/numbers/symbols/xxx"));
872         // symbols.setSignificantDigit(cldrFile.getWinningValueWithBailey("//ldml/numbers/symbols/patternDigit"));
873 
874         symbols.setDecimalSeparator(getSymbolCharacter("decimal", numberSystem));
875         // symbols.setDigit(getSymbolCharacter("patternDigit", numberSystem));
876         symbols.setExponentSeparator(getSymbolString("exponential", numberSystem));
877         symbols.setGroupingSeparator(getSymbolCharacter("group", numberSystem));
878         symbols.setInfinity(getSymbolString("infinity", numberSystem));
879         symbols.setMinusSignString(getSymbolString("minusSign", numberSystem));
880         symbols.setNaN(getSymbolString("nan", numberSystem));
881         symbols.setPatternSeparator(getSymbolCharacter("list", numberSystem));
882         symbols.setPercentString(getSymbolString("percentSign", numberSystem));
883         symbols.setPerMill(getSymbolCharacter("perMille", numberSystem));
884         symbols.setPlusSignString(getSymbolString("plusSign", numberSystem));
885         // symbols.setZeroDigit(getSymbolCharacter("nativeZeroDigit", numberSystem));
886         String digits = supplementalData.getDigits(numberSystem);
887         if (digits != null && digits.length() == 10) {
888             symbols.setZeroDigit(digits.charAt(0));
889         }
890 
891         try {
892             symbols.setMonetaryDecimalSeparator(
893                     getSymbolCharacter("currencyDecimal", numberSystem));
894         } catch (IllegalArgumentException e) {
895             symbols.setMonetaryDecimalSeparator(symbols.getDecimalSeparator());
896         }
897 
898         try {
899             symbols.setMonetaryGroupingSeparator(getSymbolCharacter("currencyGroup", numberSystem));
900         } catch (IllegalArgumentException e) {
901             symbols.setMonetaryGroupingSeparator(symbols.getGroupingSeparator());
902         }
903 
904         String prefix = "//ldml/numbers/currencyFormats/currencySpacing/beforeCurrency/";
905         beforeCurrencyMatch =
906                 new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "currencyMatch"))
907                         .freeze();
908         beforeSurroundingMatch =
909                 new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "surroundingMatch"))
910                         .freeze();
911         beforeInsertBetween = cldrFile.getWinningValueWithBailey(prefix + "insertBetween");
912         prefix = "//ldml/numbers/currencyFormats/currencySpacing/afterCurrency/";
913         afterCurrencyMatch =
914                 new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "currencyMatch"))
915                         .freeze();
916         afterSurroundingMatch =
917                 new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "surroundingMatch"))
918                         .freeze();
919         afterInsertBetween = cldrFile.getWinningValueWithBailey(prefix + "insertBetween");
920 
921         if (cachingIsEnabled) {
922             cacheDecimalFormatSymbols.put(key, symbols);
923         }
924 
925         return (DecimalFormatSymbols) symbols.clone();
926     }
927 
928     private char getSymbolCharacter(String key, String numsys) {
929         // numsys should not be null (previously resolved to defaultNumberingSystem if necessary)
930         return getSymbolString(key, numsys).charAt(0);
931     }
932 
933     private String getSymbolString(String key, String numsys) {
934         // numsys should not be null (previously resolved to defaultNumberingSystem if necessary)
935         String value = null;
936         try {
937             value =
938                     cldrFile.getWinningValueWithBailey(
939                             "//ldml/numbers/symbols[@numberSystem=\"" + numsys + "\"]/" + key);
940             if (value == null || value.length() < 1) {
941                 throw new RuntimeException();
942             }
943             return value;
944         } catch (RuntimeException e) {
945             throw new IllegalArgumentException(
946                     "Illegal value <"
947                             + value
948                             + "> at "
949                             + "//ldml/numbers/symbols[@numberSystem='"
950                             + numsys
951                             + "']/"
952                             + key);
953         }
954     }
955 
956     UnicodeSet beforeCurrencyMatch;
957     UnicodeSet beforeSurroundingMatch;
958     String beforeInsertBetween;
959     UnicodeSet afterCurrencyMatch;
960     UnicodeSet afterSurroundingMatch;
961     String afterInsertBetween;
962 
963     private String getPattern(String key1, int isCurrency) {
964         String prefix = "//ldml/numbers/";
965         String type = key1;
966         if (isCurrency == CURRENCY) type = "currency";
967         else if (key1.equals("integer")) type = "decimal";
968         String path =
969                 prefix
970                         + type
971                         + "Formats/"
972                         + type
973                         + "FormatLength/"
974                         + type
975                         + "Format[@type=\"standard\"]/pattern[@type=\"standard\"]";
976 
977         String pattern = cldrFile.getWinningValueWithBailey(path);
978         if (pattern == null)
979             throw new IllegalArgumentException(
980                     "locale: " + cldrFile.getLocaleID() + "\tpath: " + path);
981         return pattern;
982     }
983 
984     public enum Width {
985         wide,
986         abbreviated,
987         narrow
988     }
989 
990     public enum Context {
991         format,
992         stand_alone;
993 
994         @Override
995         public String toString() {
996             return name().replace('_', '-');
997         }
998     }
999 
1000     /** Format a dayPeriod string. The dayPeriodOverride, if null, will be fetched from the file. */
1001     public String formatDayPeriod(int timeInDay, Context context, Width width) {
1002         DayPeriodInfo dayPeriodInfo =
1003                 supplementalData.getDayPeriods(DayPeriodInfo.Type.format, cldrFile.getLocaleID());
1004         DayPeriod period = dayPeriodInfo.getDayPeriod(timeInDay);
1005         String dayPeriodFormatString =
1006                 getDayPeriodValue(getDayPeriodPath(period, context, width), "�", null);
1007         String result = formatDayPeriod(timeInDay, period, dayPeriodFormatString);
1008         return result;
1009     }
1010 
1011     public String getDayPeriodValue(String path, String fallback, Output<Boolean> real) {
1012         String dayPeriodFormatString = cldrFile.getStringValue(path);
1013         if (dayPeriodFormatString == null) {
1014             dayPeriodFormatString = fallback;
1015         }
1016         if (real != null) {
1017             Status status = new Status();
1018             String locale = cldrFile.getSourceLocaleID(path, status);
1019             real.value =
1020                     status.pathWhereFound.equals(path) && cldrFile.getLocaleID().equals(locale);
1021         }
1022         return dayPeriodFormatString;
1023     }
1024 
1025     public static String getDayPeriodPath(DayPeriod period, Context context, Width width) {
1026         String path =
1027                 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/dayPeriodContext[@type=\""
1028                         + context
1029                         + "\"]/dayPeriodWidth[@type=\""
1030                         + width
1031                         + "\"]/dayPeriod[@type=\""
1032                         + period
1033                         + "\"]";
1034         return path;
1035     }
1036 
1037     static final String HM_PATH =
1038             "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"hm\"]";
1039     static final String BHM_PATH =
1040             "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"Bhm\"]";
1041 
1042     public String formatDayPeriod(int timeInDay, String dayPeriodFormatString) {
1043         return formatDayPeriod(timeInDay, null, dayPeriodFormatString);
1044     }
1045 
1046     private String formatDayPeriod(int timeInDay, DayPeriod period, String dayPeriodFormatString) {
1047         String pattern = null;
1048         if ((timeInDay % 6) != 0) { // need a better way to test for this
1049             // dayPeriods other than am, pm, noon, midnight (want patterns with B)
1050             pattern = cldrFile.getStringValue(BHM_PATH);
1051             if (pattern != null) {
1052                 pattern = pattern.replace('B', '\uE000');
1053             }
1054         }
1055         if (pattern == null) {
1056             // dayPeriods am, pm, noon, midnight (want patterns with a)
1057             pattern = cldrFile.getStringValue(HM_PATH);
1058             if (pattern != null) {
1059                 pattern = pattern.replace('a', '\uE000');
1060                 // If this pattern is used for non am/pm, need to change NNBSP to regular space.
1061                 boolean fixSpace = true;
1062                 if (period != null) {
1063                     if (period == DayPeriod.am || period == DayPeriod.pm) {
1064                         fixSpace = false;
1065                     }
1066                 } else {
1067                     // All we have here is a dayPeriod string. If it is actually am/pm
1068                     // then do not fix space; but we do not know about other am/pm markers.
1069                     if (dayPeriodFormatString.equalsIgnoreCase("am")
1070                             || dayPeriodFormatString.equalsIgnoreCase("pm")) {
1071                         fixSpace = false;
1072                     }
1073                 }
1074                 if (fixSpace) {
1075                     pattern = pattern.replace('\u202F', ' ');
1076                 }
1077             }
1078         }
1079         if (pattern == null) {
1080             pattern = "h:mm \uE000";
1081         }
1082         SimpleDateFormat df = getDateFormat("gregorian", pattern);
1083         String formatted = df.format(timeInDay);
1084         String result = formatted.replace("\uE000", dayPeriodFormatString);
1085         return result;
1086     }
1087 }
1088