1 /* 2 * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package build.tools.cldrconverter; 27 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.EnumSet; 31 import java.util.HashMap; 32 import java.util.Iterator; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Objects; 36 37 class Bundle { 38 static enum Type { 39 LOCALENAMES, CURRENCYNAMES, TIMEZONENAMES, CALENDARDATA, FORMATDATA; 40 41 static EnumSet<Type> ALL_TYPES = EnumSet.of(LOCALENAMES, 42 CURRENCYNAMES, 43 TIMEZONENAMES, 44 CALENDARDATA, 45 FORMATDATA); 46 } 47 48 private final static Map<String, Bundle> bundles = new HashMap<>(); 49 50 private final static String[] NUMBER_PATTERN_KEYS = { 51 "NumberPatterns/decimal", 52 "NumberPatterns/currency", 53 "NumberPatterns/percent" 54 }; 55 56 private final static String[] NUMBER_ELEMENT_KEYS = { 57 "NumberElements/decimal", 58 "NumberElements/group", 59 "NumberElements/list", 60 "NumberElements/percent", 61 "NumberElements/zero", 62 "NumberElements/pattern", 63 "NumberElements/minus", 64 "NumberElements/exponential", 65 "NumberElements/permille", 66 "NumberElements/infinity", 67 "NumberElements/nan" 68 }; 69 70 private final static String[] TIME_PATTERN_KEYS = { 71 "DateTimePatterns/full-time", 72 "DateTimePatterns/long-time", 73 "DateTimePatterns/medium-time", 74 "DateTimePatterns/short-time", 75 }; 76 77 private final static String[] DATE_PATTERN_KEYS = { 78 "DateTimePatterns/full-date", 79 "DateTimePatterns/long-date", 80 "DateTimePatterns/medium-date", 81 "DateTimePatterns/short-date", 82 }; 83 84 private final static String[] DATETIME_PATTERN_KEYS = { 85 "DateTimePatterns/full-dateTime", 86 "DateTimePatterns/long-dateTime", 87 "DateTimePatterns/medium-dateTime", 88 "DateTimePatterns/short-dateTime", 89 }; 90 91 private final static String[] ERA_KEYS = { 92 "long.Eras", 93 "Eras", 94 "narrow.Eras" 95 }; 96 97 // Keys for individual time zone names 98 private final static String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long"; 99 private final static String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short"; 100 private final static String TZ_STD_LONG_KEY = "timezone.displayname.standard.long"; 101 private final static String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short"; 102 private final static String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long"; 103 private final static String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short"; 104 private final static String[] ZONE_NAME_KEYS = { 105 TZ_STD_LONG_KEY, 106 TZ_STD_SHORT_KEY, 107 TZ_DST_LONG_KEY, 108 TZ_DST_SHORT_KEY, 109 TZ_GEN_LONG_KEY, 110 TZ_GEN_SHORT_KEY 111 }; 112 113 private final String id; 114 private final String cldrPath; 115 private final EnumSet<Type> bundleTypes; 116 private final String currencies; 117 118 static Bundle getBundle(String id) { 119 return bundles.get(id); 120 } 121 122 @SuppressWarnings("ConvertToStringSwitch") 123 Bundle(String id, String cldrPath, String bundles, String currencies) { 124 this.id = id; 125 this.cldrPath = cldrPath; 126 if ("localenames".equals(bundles)) { 127 bundleTypes = EnumSet.of(Type.LOCALENAMES); 128 } else if ("currencynames".equals(bundles)) { 129 bundleTypes = EnumSet.of(Type.CURRENCYNAMES); 130 } else { 131 bundleTypes = Type.ALL_TYPES; 132 } 133 if (currencies == null) { 134 currencies = "local"; 135 } 136 this.currencies = currencies; 137 addBundle(); 138 } 139 140 private void addBundle() { 141 Bundle.bundles.put(id, this); 142 } 143 144 String getID() { 145 return id; 146 } 147 148 boolean isRoot() { 149 return "root".equals(id); 150 } 151 152 String getCLDRPath() { 153 return cldrPath; 154 } 155 156 EnumSet<Type> getBundleTypes() { 157 return bundleTypes; 158 } 159 160 String getCurrencies() { 161 return currencies; 162 } 163 164 /** 165 * Generate a map that contains all the data that should be 166 * visible for the bundle's locale 167 */ 168 Map<String, Object> getTargetMap() throws Exception { 169 String[] cldrBundles = getCLDRPath().split(","); 170 171 // myMap contains resources for id. 172 Map<String, Object> myMap = new HashMap<>(); 173 int index; 174 for (index = 0; index < cldrBundles.length; index++) { 175 if (cldrBundles[index].equals(id)) { 176 myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index])); 177 CLDRConverter.handleAliases(myMap); 178 break; 179 } 180 } 181 182 // parentsMap contains resources from id's parents. 183 Map<String, Object> parentsMap = new HashMap<>(); 184 for (int i = cldrBundles.length - 1; i > index; i--) { 185 if (!("no".equals(cldrBundles[i]) || cldrBundles[i].startsWith("no_"))) { 186 parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i])); 187 CLDRConverter.handleAliases(parentsMap); 188 } 189 } 190 // Duplicate myMap as parentsMap for "root" so that the 191 // fallback works. This is a hack, though. 192 if ("root".equals(cldrBundles[0])) { 193 assert parentsMap.isEmpty(); 194 parentsMap.putAll(myMap); 195 } 196 197 // merge individual strings into arrays 198 199 // if myMap has any of the NumberPatterns members 200 for (String k : NUMBER_PATTERN_KEYS) { 201 if (myMap.containsKey(k)) { 202 String[] numberPatterns = new String[NUMBER_PATTERN_KEYS.length]; 203 for (int i = 0; i < NUMBER_PATTERN_KEYS.length; i++) { 204 String key = NUMBER_PATTERN_KEYS[i]; 205 String value = (String) myMap.remove(key); 206 if (value == null) { 207 value = (String) parentsMap.remove(key); 208 } 209 if (value.length() == 0) { 210 CLDRConverter.warning("empty pattern for " + key); 211 } 212 numberPatterns[i] = value; 213 } 214 myMap.put("NumberPatterns", numberPatterns); 215 break; 216 } 217 } 218 219 // if myMap has any of NUMBER_ELEMENT_KEYS, create a complete NumberElements. 220 String defaultScript = (String) myMap.get("DefaultNumberingSystem"); 221 @SuppressWarnings("unchecked") 222 List<String> scripts = (List<String>) myMap.get("numberingScripts"); 223 if (defaultScript == null && scripts != null) { 224 // Some locale data has no default script for numbering even with mutiple scripts. 225 // Take the first one as default in that case. 226 defaultScript = scripts.get(0); 227 myMap.put("DefaultNumberingSystem", defaultScript); 228 } 229 if (scripts != null) { 230 for (String script : scripts) { 231 for (String k : NUMBER_ELEMENT_KEYS) { 232 String[] numberElements = new String[NUMBER_ELEMENT_KEYS.length]; 233 for (int i = 0; i < NUMBER_ELEMENT_KEYS.length; i++) { 234 String key = script + "." + NUMBER_ELEMENT_KEYS[i]; 235 String value = (String) myMap.remove(key); 236 if (value == null) { 237 if (key.endsWith("/pattern")) { 238 value = "#"; 239 } else { 240 value = (String) parentsMap.get(key); 241 if (value == null) { 242 // the last resort is "latn" 243 key = "latn." + NUMBER_ELEMENT_KEYS[i]; 244 value = (String) parentsMap.get(key); 245 if (value == null) { 246 throw new InternalError("NumberElements: null for " + key); 247 } 248 } 249 } 250 } 251 numberElements[i] = value; 252 } 253 myMap.put(script + "." + "NumberElements", numberElements); 254 break; 255 } 256 } 257 } 258 259 // another hack: parentsMap is not used for date-time resources. 260 if ("root".equals(id)) { 261 parentsMap = null; 262 } 263 264 for (CalendarType calendarType : CalendarType.values()) { 265 String calendarPrefix = calendarType.keyElementName(); 266 // handle multiple inheritance for month and day names 267 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames"); 268 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations"); 269 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNarrows"); 270 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames"); 271 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations"); 272 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNarrows"); 273 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers"); 274 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "narrow.AmPmMarkers"); 275 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNames"); 276 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterAbbreviations"); 277 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNarrows"); 278 279 adjustEraNames(myMap, calendarType); 280 281 handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns"); 282 handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns"); 283 handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns"); 284 } 285 286 // First, weed out any empty timezone or metazone names from myMap. 287 // Fill in any missing abbreviations if locale is "en". 288 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 289 String key = it.next(); 290 if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX) 291 || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 292 @SuppressWarnings("unchecked") 293 Map<String, String> nameMap = (Map<String, String>) myMap.get(key); 294 if (nameMap.isEmpty()) { 295 // Some zones have only exemplarCity, which become empty. 296 // Remove those from the map. 297 it.remove(); 298 continue; 299 } 300 301 if (id.startsWith("en")) { 302 fillInAbbrs(key, nameMap); 303 } 304 } 305 } 306 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 307 String key = it.next(); 308 if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX) 309 || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 310 @SuppressWarnings("unchecked") 311 Map<String, String> nameMap = (Map<String, String>) myMap.get(key); 312 // Convert key/value pairs to an array. 313 String[] names = new String[ZONE_NAME_KEYS.length]; 314 int ix = 0; 315 for (String nameKey : ZONE_NAME_KEYS) { 316 String name = nameMap.get(nameKey); 317 if (name == null) { 318 @SuppressWarnings("unchecked") 319 Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key); 320 if (parentNames != null) { 321 name = parentNames.get(nameKey); 322 } 323 } 324 names[ix++] = name; 325 } 326 if (hasNulls(names)) { 327 String metaKey = toMetaZoneKey(key); 328 if (metaKey != null) { 329 Object obj = myMap.get(metaKey); 330 if (obj instanceof String[]) { 331 String[] metaNames = (String[]) obj; 332 for (int i = 0; i < names.length; i++) { 333 if (names[i] == null) { 334 names[i] = metaNames[i]; 335 } 336 } 337 } else if (obj instanceof Map) { 338 @SuppressWarnings("unchecked") 339 Map<String, String> m = (Map<String, String>) obj; 340 for (int i = 0; i < names.length; i++) { 341 if (names[i] == null) { 342 names[i] = m.get(ZONE_NAME_KEYS[i]); 343 } 344 } 345 } 346 } 347 // If there are still any nulls, try filling in them from en data. 348 if (hasNulls(names) && !id.equals("en")) { 349 @SuppressWarnings("unchecked") 350 String[] enNames = (String[]) Bundle.getBundle("en").getTargetMap().get(key); 351 if (enNames == null) { 352 if (metaKey != null) { 353 @SuppressWarnings("unchecked") 354 String[] metaNames = (String[]) Bundle.getBundle("en").getTargetMap().get(metaKey); 355 enNames = metaNames; 356 } 357 } 358 if (enNames != null) { 359 for (int i = 0; i < names.length; i++) { 360 if (names[i] == null) { 361 names[i] = enNames[i]; 362 } 363 } 364 } 365 // If there are still nulls, give up names. 366 if (hasNulls(names)) { 367 names = null; 368 } 369 } 370 } 371 // replace the Map with the array 372 if (names != null) { 373 myMap.put(key, names); 374 } else { 375 it.remove(); 376 } 377 } 378 } 379 380 // Remove all duplicates 381 if (Objects.nonNull(parentsMap)) { 382 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 383 String key = it.next(); 384 if (Objects.deepEquals(parentsMap.get(key), myMap.get(key))) { 385 it.remove(); 386 } 387 } 388 } 389 390 return myMap; 391 } 392 393 private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) { 394 String formatKey = key + "/format"; 395 Object format = map.get(formatKey); 396 if (format != null) { 397 map.remove(formatKey); 398 map.put(key, format); 399 if (fillInElements(parents, formatKey, format)) { 400 map.remove(key); 401 } 402 } 403 String standaloneKey = key + "/stand-alone"; 404 Object standalone = map.get(standaloneKey); 405 if (standalone != null) { 406 map.remove(standaloneKey); 407 String realKey = key; 408 if (format != null) { 409 realKey = "standalone." + key; 410 } 411 map.put(realKey, standalone); 412 if (fillInElements(parents, standaloneKey, standalone)) { 413 map.remove(realKey); 414 } 415 } 416 } 417 418 /** 419 * Fills in any empty elements with its parent element. Returns true if the resulting array is 420 * identical to its parent array. 421 * 422 * @param parents 423 * @param key 424 * @param value 425 * @return true if the resulting array is identical to its parent array. 426 */ 427 private boolean fillInElements(Map<String, Object> parents, String key, Object value) { 428 if (parents == null) { 429 return false; 430 } 431 if (value instanceof String[]) { 432 Object pvalue = parents.get(key); 433 if (pvalue != null && pvalue instanceof String[]) { 434 String[] strings = (String[]) value; 435 String[] pstrings = (String[]) pvalue; 436 for (int i = 0; i < strings.length; i++) { 437 if (strings[i] == null || strings[i].length() == 0) { 438 strings[i] = pstrings[i]; 439 } 440 } 441 return Arrays.equals(strings, pstrings); 442 } 443 } 444 return false; 445 } 446 447 /* 448 * Adjusts String[] for era names because JRE's Calendars use different 449 * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars. 450 */ 451 private void adjustEraNames(Map<String, Object> map, CalendarType type) { 452 String[][] eraNames = new String[ERA_KEYS.length][]; 453 String[] realKeys = new String[ERA_KEYS.length]; 454 int index = 0; 455 for (String key : ERA_KEYS) { 456 String realKey = type.keyElementName() + key; 457 String[] value = (String[]) map.get(realKey); 458 if (value != null) { 459 switch (type) { 460 case GREGORIAN: 461 break; 462 463 case JAPANESE: 464 { 465 String[] newValue = new String[value.length + 1]; 466 String[] julianEras = (String[]) map.get(key); 467 if (julianEras != null && julianEras.length >= 2) { 468 newValue[0] = julianEras[1]; 469 } else { 470 newValue[0] = ""; 471 } 472 System.arraycopy(value, 0, newValue, 1, value.length); 473 value = newValue; 474 } 475 break; 476 477 case BUDDHIST: 478 // Replace the value 479 value = new String[] {"BC", value[0]}; 480 break; 481 482 case ISLAMIC: 483 // Replace the value 484 value = new String[] {"", value[0]}; 485 break; 486 } 487 if (!key.equals(realKey)) { 488 map.put(realKey, value); 489 } 490 } 491 realKeys[index] = realKey; 492 eraNames[index++] = value; 493 } 494 for (int i = 0; i < eraNames.length; i++) { 495 if (eraNames[i] == null) { 496 map.put(realKeys[i], null); 497 } 498 } 499 } 500 501 private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap, 502 CalendarType calendarType, String name) { 503 String calendarPrefix = calendarType.keyElementName(); 504 for (String k : patternKeys) { 505 if (myMap.containsKey(calendarPrefix + k)) { 506 int len = patternKeys.length; 507 List<String> rawPatterns = new ArrayList<>(len); 508 List<String> patterns = new ArrayList<>(len); 509 for (int i = 0; i < len; i++) { 510 String key = calendarPrefix + patternKeys[i]; 511 String pattern = (String) myMap.remove(key); 512 if (pattern == null) { 513 pattern = (String) parentsMap.remove(key); 514 } 515 rawPatterns.add(i, pattern); 516 if (pattern != null) { 517 patterns.add(i, translateDateFormatLetters(calendarType, pattern)); 518 } else { 519 patterns.add(i, null); 520 } 521 } 522 // If patterns is empty or has any nulls, discard patterns. 523 if (patterns.isEmpty()) { 524 return; 525 } 526 String key = calendarPrefix + name; 527 if (!rawPatterns.equals(patterns)) { 528 myMap.put("java.time." + key, rawPatterns.toArray(new String[len])); 529 } 530 myMap.put(key, patterns.toArray(new String[len])); 531 break; 532 } 533 } 534 } 535 536 private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) { 537 String pattern = cldrFormat; 538 int length = pattern.length(); 539 boolean inQuote = false; 540 StringBuilder jrePattern = new StringBuilder(length); 541 int count = 0; 542 char lastLetter = 0; 543 544 for (int i = 0; i < length; i++) { 545 char c = pattern.charAt(i); 546 547 if (c == '\'') { 548 // '' is treated as a single quote regardless of being 549 // in a quoted section. 550 if ((i + 1) < length) { 551 char nextc = pattern.charAt(i + 1); 552 if (nextc == '\'') { 553 i++; 554 if (count != 0) { 555 convert(calendarType, lastLetter, count, jrePattern); 556 lastLetter = 0; 557 count = 0; 558 } 559 jrePattern.append("''"); 560 continue; 561 } 562 } 563 if (!inQuote) { 564 if (count != 0) { 565 convert(calendarType, lastLetter, count, jrePattern); 566 lastLetter = 0; 567 count = 0; 568 } 569 inQuote = true; 570 } else { 571 inQuote = false; 572 } 573 jrePattern.append(c); 574 continue; 575 } 576 if (inQuote) { 577 jrePattern.append(c); 578 continue; 579 } 580 if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { 581 if (count != 0) { 582 convert(calendarType, lastLetter, count, jrePattern); 583 lastLetter = 0; 584 count = 0; 585 } 586 jrePattern.append(c); 587 continue; 588 } 589 590 if (lastLetter == 0 || lastLetter == c) { 591 lastLetter = c; 592 count++; 593 continue; 594 } 595 convert(calendarType, lastLetter, count, jrePattern); 596 lastLetter = c; 597 count = 1; 598 } 599 600 if (inQuote) { 601 throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat); 602 } 603 604 if (count != 0) { 605 convert(calendarType, lastLetter, count, jrePattern); 606 } 607 if (cldrFormat.contentEquals(jrePattern)) { 608 return cldrFormat; 609 } 610 return jrePattern.toString(); 611 } 612 613 private String toMetaZoneKey(String tzKey) { 614 if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) { 615 String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 616 String meta = CLDRConverter.handlerMetaZones.get(tz); 617 if (meta != null) { 618 return CLDRConverter.METAZONE_ID_PREFIX + meta; 619 } 620 } 621 return null; 622 } 623 624 private void fillInAbbrs(String key, Map<String, String> map) { 625 fillInAbbrs(TZ_STD_LONG_KEY, TZ_STD_SHORT_KEY, map); 626 fillInAbbrs(TZ_DST_LONG_KEY, TZ_DST_SHORT_KEY, map); 627 fillInAbbrs(TZ_GEN_LONG_KEY, TZ_GEN_SHORT_KEY, map); 628 629 // If the standard std is "Standard Time" and daylight std is "Summer Time", 630 // replace the standard std with the generic std to avoid using 631 // the same abbrivation except for Australia time zone names. 632 String std = map.get(TZ_STD_SHORT_KEY); 633 String dst = map.get(TZ_DST_SHORT_KEY); 634 String gen = map.get(TZ_GEN_SHORT_KEY); 635 if (std != null) { 636 if (dst == null) { 637 // if dst is null, create long and short names from the standard 638 // std. ("Something Standard Time" to "Something Daylight Time", 639 // or "Something Time" to "Something Summer Time") 640 String name = map.get(TZ_STD_LONG_KEY); 641 if (name != null) { 642 if (name.contains("Standard Time")) { 643 name = name.replace("Standard Time", "Daylight Time"); 644 } else if (name.endsWith("Mean Time")) { 645 if (!name.startsWith("Greenwich ")) { 646 name = name.replace("Mean Time", "Summer Time"); 647 } 648 } else if (name.endsWith(" Time")) { 649 name = name.replace(" Time", " Summer Time"); 650 } 651 map.put(TZ_DST_LONG_KEY, name); 652 fillInAbbrs(TZ_DST_LONG_KEY, TZ_DST_SHORT_KEY, map); 653 } 654 } 655 if (gen == null) { 656 String name = map.get(TZ_STD_LONG_KEY); 657 if (name != null) { 658 if (name.endsWith("Standard Time")) { 659 name = name.replace("Standard Time", "Time"); 660 } else if (name.endsWith("Mean Time")) { 661 if (!name.startsWith("Greenwich ")) { 662 name = name.replace("Mean Time", "Time"); 663 } 664 } 665 map.put(TZ_GEN_LONG_KEY, name); 666 fillInAbbrs(TZ_GEN_LONG_KEY, TZ_GEN_SHORT_KEY, map); 667 } 668 } 669 } 670 } 671 672 private void fillInAbbrs(String longKey, String shortKey, Map<String, String> map) { 673 String abbr = map.get(shortKey); 674 if (abbr == null) { 675 String name = map.get(longKey); 676 if (name != null) { 677 abbr = toAbbr(name); 678 if (abbr != null) { 679 map.put(shortKey, abbr); 680 } 681 } 682 } 683 } 684 685 private String toAbbr(String name) { 686 String[] substrs = name.split("\\s+"); 687 StringBuilder sb = new StringBuilder(); 688 for (String s : substrs) { 689 char c = s.charAt(0); 690 if (c >= 'A' && c <= 'Z') { 691 sb.append(c); 692 } 693 } 694 return sb.length() > 0 ? sb.toString() : null; 695 } 696 697 private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { 698 switch (cldrLetter) { 699 case 'G': 700 if (calendarType != CalendarType.GREGORIAN) { 701 // Adjust the number of 'G's for JRE SimpleDateFormat 702 if (count == 5) { 703 // CLDR narrow -> JRE short 704 count = 1; 705 } else if (count == 1) { 706 // CLDR abbr -> JRE long 707 count = 4; 708 } 709 } 710 appendN(cldrLetter, count, sb); 711 break; 712 713 // TODO: support 'c' and 'e' in JRE SimpleDateFormat 714 // Use 'u' and 'E' for now. 715 case 'c': 716 case 'e': 717 switch (count) { 718 case 1: 719 sb.append('u'); 720 break; 721 case 3: 722 case 4: 723 appendN('E', count, sb); 724 break; 725 case 5: 726 appendN('E', 3, sb); 727 break; 728 } 729 break; 730 731 case 'l': 732 // 'l' is deprecated as a pattern character. Should be ignored. 733 break; 734 735 case 'u': 736 // Use 'y' for now. 737 appendN('y', count, sb); 738 break; 739 740 case 'v': 741 case 'V': 742 appendN('z', count, sb); 743 break; 744 745 case 'Z': 746 if (count == 4 || count == 5) { 747 sb.append("XXX"); 748 } 749 break; 750 751 case 'U': 752 case 'q': 753 case 'Q': 754 case 'g': 755 case 'j': 756 case 'A': 757 throw new InternalError(String.format("Unsupported letter: '%c', count=%d, id=%s%n", 758 cldrLetter, count, id)); 759 default: 760 appendN(cldrLetter, count, sb); 761 break; 762 } 763 } 764 765 private void appendN(char c, int n, StringBuilder sb) { 766 for (int i = 0; i < n; i++) { 767 sb.append(c); 768 } 769 } 770 771 private static boolean hasNulls(Object[] array) { 772 for (int i = 0; i < array.length; i++) { 773 if (array[i] == null) { 774 return true; 775 } 776 } 777 return false; 778 } 779 }