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