• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             if (currencySymbol == null) {
646                 throw new NullPointerException(
647                     cldrFile.getSourceLocation(prefix + "symbol") +
648                     ": " + cldrFile.getLocaleID()+ ": " +
649                     ": null currencySymbol for " + prefix + "symbol");
650             }
651             String currencyDecimal = cldrFile.getWinningValueWithBailey(prefix + "decimal");
652             if (currencyDecimal != null) {
653                 (symbols = cloneIfNeeded(symbols)).setMonetaryDecimalSeparator(currencyDecimal.charAt(0));
654             }
655             String currencyPattern = cldrFile.getWinningValueWithBailey(prefix + "pattern");
656             if (currencyPattern != null) {
657                 pattern = currencyPattern;
658             }
659 
660             String currencyGrouping = cldrFile.getWinningValueWithBailey(prefix + "grouping");
661             if (currencyGrouping != null) {
662                 (symbols = cloneIfNeeded(symbols)).setMonetaryGroupingSeparator(currencyGrouping.charAt(0));
663             }
664 
665             // <decimal>,</decimal>
666             // <group>.</group>
667 
668             // TODO This is a hack for now, since I am ignoring the possibility of quoted text next to the symbol
669             if (pattern.contains(";")) { // multi pattern
670                 String[] pieces = pattern.split(";");
671                 for (int i = 0; i < pieces.length; ++i) {
672                     pieces[i] = fixCurrencySpacing(pieces[i], currencySymbol);
673                 }
674                 pattern = org.unicode.cldr.util.CldrUtility.join(pieces, ";");
675             } else {
676                 pattern = fixCurrencySpacing(pattern, currencySymbol);
677             }
678 
679             CurrencyNumberInfo info = supplementalData.getCurrencyNumberInfo(key1);
680 
681             mc = new MyCurrency(key1,
682                 currencySymbol,
683                 cldrFile.getWinningValueWithBailey(prefix + "displayName"),
684                 info);
685 
686             // String possible = null;
687             // possible = cldrFile.getWinningValueWithBailey(prefix + "decimal");
688             // symbols.setMonetaryDecimalSeparator(possible != null ? possible.charAt(0) :
689             // symbols.getDecimalSeparator());
690             // if ((possible = cldrFile.getWinningValueWithBailey(prefix + "pattern")) != null) pattern = possible;
691             // if ((possible = cldrFile.getWinningValueWithBailey(prefix + "group")) != null)
692             // symbols.setGroupingSeparator(possible.charAt(0));
693             // ;
694         }
695         result = new DecimalFormat(pattern, symbols);
696         if (mc != null) {
697             result.setCurrency(mc);
698             result.setMaximumFractionDigits(mc.getDefaultFractionDigits());
699             result.setMinimumFractionDigits(mc.getDefaultFractionDigits());
700         } else {
701             result.setCurrency(NO_CURRENCY);
702         }
703 
704         if (false) {
705             System.out.println("creating " + ulocale + "\tkey: " + key + "\tpattern "
706                 + pattern + "\tresult: " + result.toPattern() + "\t0=>" + result.format(0));
707             DecimalFormat n2 = (DecimalFormat) NumberFormat.getScientificInstance(ulocale);
708             System.out.println("\tresult: " + n2.toPattern() + "\t0=>" + n2.format(0));
709         }
710         if (kind == OTHER_KEY && key1.equals("integer")) {
711             result.setMaximumFractionDigits(0);
712             result.setDecimalSeparatorAlwaysShown(false);
713             result.setParseIntegerOnly(true);
714         }
715         cacheNumberFormats.put(key, result);
716         return (DecimalFormat) result.clone();
717     }
718 
719     private String fixCurrencySpacing(String pattern, String symbol) {
720         int startPos = pattern.indexOf('\u00a4');
721         if (startPos > 0
722             && beforeCurrencyMatch.contains(UTF16.charAt(symbol, 0))) {
723             int ch = UTF16.charAt(pattern, startPos - 1);
724             if (ch == '#') ch = '0';// fix pattern
725             if (beforeSurroundingMatch.contains(ch)) {
726                 pattern = pattern.substring(0, startPos) + beforeInsertBetween + pattern.substring(startPos);
727             }
728         }
729         int endPos = pattern.lastIndexOf('\u00a4') + 1;
730         if (endPos < pattern.length()
731             && afterCurrencyMatch.contains(UTF16.charAt(symbol, symbol.length() - 1))) {
732             int ch = UTF16.charAt(pattern, endPos);
733             if (ch == '#') ch = '0';// fix pattern
734             if (afterSurroundingMatch.contains(ch)) {
735                 pattern = pattern.substring(0, endPos) + afterInsertBetween + pattern.substring(endPos);
736             }
737         }
738         return pattern;
739     }
740 
741     private DecimalFormatSymbols cloneIfNeeded(DecimalFormatSymbols symbols) {
742         if (symbols == _getDecimalFormatSymbols(null)) {
743             return (DecimalFormatSymbols) symbols.clone();
744         }
745         return symbols;
746     }
747 
748     public DecimalFormatSymbols getDecimalFormatSymbols(String numberSystem) {
749         return (DecimalFormatSymbols) _getDecimalFormatSymbols(numberSystem).clone();
750     }
751 
752     private DecimalFormatSymbols _getDecimalFormatSymbols(String numberSystem) {
753         String key = (numberSystem == null) ? cldrFile.getLocaleID() : cldrFile.getLocaleID() + "@numbers="
754             + numberSystem;
755         DecimalFormatSymbols symbols = cacheDecimalFormatSymbols.get(key);
756         if (symbols != null) {
757             return (DecimalFormatSymbols) symbols.clone();
758         }
759 
760         symbols = new DecimalFormatSymbols();
761         if (numberSystem == null) {
762             numberSystem = cldrFile.getWinningValueWithBailey("//ldml/numbers/defaultNumberingSystem");
763         }
764 
765         // currently constants
766         // symbols.setPadEscape(cldrFile.getWinningValueWithBailey("//ldml/numbers/symbols/xxx"));
767         // symbols.setSignificantDigit(cldrFile.getWinningValueWithBailey("//ldml/numbers/symbols/patternDigit"));
768 
769         symbols.setDecimalSeparator(getSymbolCharacter("decimal", numberSystem));
770         // symbols.setDigit(getSymbolCharacter("patternDigit", numberSystem));
771         symbols.setExponentSeparator(getSymbolString("exponential", numberSystem));
772         symbols.setGroupingSeparator(getSymbolCharacter("group", numberSystem));
773         symbols.setInfinity(getSymbolString("infinity", numberSystem));
774         symbols.setMinusSignString(getSymbolString("minusSign", numberSystem));
775         symbols.setNaN(getSymbolString("nan", numberSystem));
776         symbols.setPatternSeparator(getSymbolCharacter("list", numberSystem));
777         symbols.setPercentString(getSymbolString("percentSign", numberSystem));
778         symbols.setPerMill(getSymbolCharacter("perMille", numberSystem));
779         symbols.setPlusSignString(getSymbolString("plusSign", numberSystem));
780         // symbols.setZeroDigit(getSymbolCharacter("nativeZeroDigit", numberSystem));
781         String digits = supplementalData.getDigits(numberSystem);
782         if (digits != null && digits.length() == 10) {
783             symbols.setZeroDigit(digits.charAt(0));
784         }
785 
786         try {
787             symbols.setMonetaryDecimalSeparator(getSymbolCharacter("currencyDecimal", numberSystem));
788         } catch (IllegalArgumentException e) {
789             symbols.setMonetaryDecimalSeparator(symbols.getDecimalSeparator());
790         }
791 
792         try {
793             symbols.setMonetaryGroupingSeparator(getSymbolCharacter("currencyGroup", numberSystem));
794         } catch (IllegalArgumentException e) {
795             symbols.setMonetaryGroupingSeparator(symbols.getGroupingSeparator());
796         }
797 
798         String prefix = "//ldml/numbers/currencyFormats/currencySpacing/beforeCurrency/";
799         beforeCurrencyMatch = new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "currencyMatch")).freeze();
800         beforeSurroundingMatch = new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "surroundingMatch")).freeze();
801         beforeInsertBetween = cldrFile.getWinningValueWithBailey(prefix + "insertBetween");
802         prefix = "//ldml/numbers/currencyFormats/currencySpacing/afterCurrency/";
803         afterCurrencyMatch = new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "currencyMatch")).freeze();
804         afterSurroundingMatch = new UnicodeSet(cldrFile.getWinningValueWithBailey(prefix + "surroundingMatch")).freeze();
805         afterInsertBetween = cldrFile.getWinningValueWithBailey(prefix + "insertBetween");
806 
807         cacheDecimalFormatSymbols.put(key, symbols);
808 
809         return (DecimalFormatSymbols) symbols.clone();
810     }
811 
812     private char getSymbolCharacter(String key, String numsys) {
813         // numsys should not be null (previously resolved to defaultNumberingSystem if necessary)
814         return getSymbolString(key, numsys).charAt(0);
815     }
816 
817     // TODO no longer used now that http://bugs.icu-project.org/trac/ticket/10368 is done.
818     private char getHackSymbolCharacter(String key, String numsys) {
819         String minusString = getSymbolString(key, numsys);
820         char minusSign = (minusString.length() > 1 && isBidiMark(minusString.charAt(0))) ? minusString.charAt(1) : minusString.charAt(0);
821         return minusSign;
822     }
823 
824     private static boolean isBidiMark(char c) {
825         return (c == '\u200E' || c == '\u200F' || c == '\u061C');
826     }
827 
828     private String getSymbolString(String key, String numsys) {
829         // numsys should not be null (previously resolved to defaultNumberingSystem if necessary)
830         String value = null;
831         try {
832             value = cldrFile.getWinningValueWithBailey("//ldml/numbers/symbols[@numberSystem=\"" + numsys + "\"]/" + key);
833             if (value == null || value.length() < 1) {
834                 throw new RuntimeException();
835             }
836             return value;
837         } catch (RuntimeException e) {
838             throw new IllegalArgumentException("Illegal value <" + value + "> at "
839                 + "//ldml/numbers/symbols[@numberSystem='" + numsys + "']/" + key);
840         }
841     }
842 
843     UnicodeSet beforeCurrencyMatch;
844     UnicodeSet beforeSurroundingMatch;
845     String beforeInsertBetween;
846     UnicodeSet afterCurrencyMatch;
847     UnicodeSet afterSurroundingMatch;
848     String afterInsertBetween;
849 
850     private String getPattern(String key1, int isCurrency) {
851         String prefix = "//ldml/numbers/";
852         String type = key1;
853         if (isCurrency == CURRENCY)
854             type = "currency";
855         else if (key1.equals("integer")) type = "decimal";
856         String path = prefix
857             + type + "Formats/"
858             + type + "FormatLength/"
859             + type + "Format[@type=\"standard\"]/pattern[@type=\"standard\"]";
860 
861         String pattern = cldrFile.getWinningValueWithBailey(path);
862         if (pattern == null)
863             throw new IllegalArgumentException("locale: " + cldrFile.getLocaleID() + "\tpath: " + path);
864         return pattern;
865     }
866 
867     public enum Width {
868         wide, abbreviated, narrow
869     }
870 
871     public enum Context {
872         format, stand_alone;
873         @Override
874         public String toString() {
875             return name().replace('_', '-');
876         }
877     }
878 
879     /**
880      * Format a dayPeriod string. The dayPeriodOverride, if null, will be fetched from the file.
881      * @param timeInDay
882      * @param dayPeriodString
883      * @return
884      */
885     public String formatDayPeriod(int timeInDay, Context context, Width width) {
886         DayPeriodInfo dayPeriodInfo = supplementalData.getDayPeriods(DayPeriodInfo.Type.format, cldrFile.getLocaleID());
887         DayPeriod period = dayPeriodInfo.getDayPeriod(timeInDay);
888         String dayPeriodFormatString = getDayPeriodValue(getDayPeriodPath(period, context, width), "�", null);
889         String result = formatDayPeriod(timeInDay, period, dayPeriodFormatString);
890         return result;
891     }
892 
893     public String getDayPeriodValue(String path, String fallback, Output<Boolean> real) {
894         String dayPeriodFormatString = cldrFile.getStringValue(path);
895         if (dayPeriodFormatString == null) {
896             dayPeriodFormatString = fallback;
897         }
898         if (real != null) {
899             Status status = new Status();
900             String locale = cldrFile.getSourceLocaleID(path, status);
901             real.value = status.pathWhereFound.equals(path) && cldrFile.getLocaleID().equals(locale);
902         }
903         return dayPeriodFormatString;
904     }
905 
906     public static String getDayPeriodPath(DayPeriod period, Context context, Width width) {
907         String path = "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/dayPeriodContext[@type=\""
908             + context
909             + "\"]/dayPeriodWidth[@type=\""
910             + width
911             + "\"]/dayPeriod[@type=\""
912             + period
913             + "\"]";
914         return path;
915     }
916 
917     static final String SHORT_PATH = "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/timeFormats/timeFormatLength[@type=\"short\"]/timeFormat[@type=\"standard\"]/pattern[@type=\"standard\"]";
918     static final String HM_PATH = "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"hm\"]";
919     static final String BHM_PATH = "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"Bhm\"]";
920 
921     public String formatDayPeriod(int timeInDay, String dayPeriodFormatString) {
922         return formatDayPeriod(timeInDay, null, dayPeriodFormatString);
923     }
924 
925     private String formatDayPeriod(int timeInDay, DayPeriod period, String dayPeriodFormatString) {
926         String pattern = null;
927         if ((timeInDay % 6) != 0) { // need a better way to test for this
928             // dayPeriods other than am, pm, noon, midnight (want patterns with B)
929             pattern = cldrFile.getStringValue(BHM_PATH);
930             if (pattern != null) {
931                 pattern = pattern.replace('B', '\uE000');
932             }
933         }
934         if (pattern == null) {
935             // dayPeriods am, pm, noon, midnight (want patterns with a)
936             pattern = cldrFile.getStringValue(HM_PATH);
937             if (pattern != null) {
938                 pattern = pattern.replace('a', '\uE000');
939                 // If this pattern is used for non am/pm, need to change NNBSP to regular space.
940                 boolean fixSpace = true;
941                 if (period != null) {
942                     if (period == DayPeriod.am || period == DayPeriod.pm) {
943                         fixSpace = false;
944                     }
945                 } else {
946                     // All we have here is a dayPeriod string. If it is actually am/pm
947                     // then do not fix space; but we do not know about other am/pm markers.
948                     if (dayPeriodFormatString.equalsIgnoreCase("am") || dayPeriodFormatString.equalsIgnoreCase("pm")) {
949                         fixSpace = false;
950                     }
951                 }
952                 if (fixSpace) {
953                     pattern = pattern.replace('\u202F', ' ');
954                 }
955             }
956         }
957         if (pattern == null) {
958             pattern = "h:mm \uE000";
959         }
960         SimpleDateFormat df = getDateFormat("gregorian", pattern);
961         String formatted = df.format(timeInDay);
962         String result = formatted.replace("\uE000", dayPeriodFormatString);
963         return result;
964     }
965 
966     public String getMinusSign(String numberSystem) {
967         return _getDecimalFormatSymbols(numberSystem).getMinusSignString();
968     }
969 }
970