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