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