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