1 /* 2 * Copyright (c) 2012, 2017, 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 private Map<String, Object> targetMap; 118 119 static Bundle getBundle(String id) { 120 return bundles.get(id); 121 } 122 123 @SuppressWarnings("ConvertToStringSwitch") 124 Bundle(String id, String cldrPath, String bundles, String currencies) { 125 this.id = id; 126 this.cldrPath = cldrPath; 127 if ("localenames".equals(bundles)) { 128 bundleTypes = EnumSet.of(Type.LOCALENAMES); 129 } else if ("currencynames".equals(bundles)) { 130 bundleTypes = EnumSet.of(Type.CURRENCYNAMES); 131 } else { 132 bundleTypes = Type.ALL_TYPES; 133 } 134 if (currencies == null) { 135 currencies = "local"; 136 } 137 this.currencies = currencies; 138 addBundle(); 139 } 140 141 private void addBundle() { 142 Bundle.bundles.put(id, this); 143 } 144 145 String getID() { 146 return id; 147 } 148 149 String getJavaID() { 150 // Tweak ISO compatibility for bundle generation 151 return id.replaceFirst("^he", "iw") 152 .replaceFirst("^id", "in") 153 .replaceFirst("^yi", "ji"); 154 } 155 156 boolean isRoot() { 157 return "root".equals(id); 158 } 159 160 String getCLDRPath() { 161 return cldrPath; 162 } 163 164 EnumSet<Type> getBundleTypes() { 165 return bundleTypes; 166 } 167 168 String getCurrencies() { 169 return currencies; 170 } 171 172 /** 173 * Generate a map that contains all the data that should be 174 * visible for the bundle's locale 175 */ 176 Map<String, Object> getTargetMap() throws Exception { 177 if (targetMap != null) { 178 return targetMap; 179 } 180 181 String[] cldrBundles = getCLDRPath().split(","); 182 183 // myMap contains resources for id. 184 Map<String, Object> myMap = new HashMap<>(); 185 int index; 186 for (index = 0; index < cldrBundles.length; index++) { 187 if (cldrBundles[index].equals(id)) { 188 myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index])); 189 CLDRConverter.handleAliases(myMap); 190 break; 191 } 192 } 193 194 // parentsMap contains resources from id's parents. 195 Map<String, Object> parentsMap = new HashMap<>(); 196 for (int i = cldrBundles.length - 1; i > index; i--) { 197 if (!("no".equals(cldrBundles[i]) || cldrBundles[i].startsWith("no_"))) { 198 parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i])); 199 CLDRConverter.handleAliases(parentsMap); 200 } 201 } 202 // Duplicate myMap as parentsMap for "root" so that the 203 // fallback works. This is a hack, though. 204 if ("root".equals(cldrBundles[0])) { 205 assert parentsMap.isEmpty(); 206 parentsMap.putAll(myMap); 207 } 208 209 // merge individual strings into arrays 210 211 // if myMap has any of the NumberPatterns members 212 for (String k : NUMBER_PATTERN_KEYS) { 213 if (myMap.containsKey(k)) { 214 String[] numberPatterns = new String[NUMBER_PATTERN_KEYS.length]; 215 for (int i = 0; i < NUMBER_PATTERN_KEYS.length; i++) { 216 String key = NUMBER_PATTERN_KEYS[i]; 217 String value = (String) myMap.remove(key); 218 if (value == null) { 219 value = (String) parentsMap.remove(key); 220 } 221 if (value.length() == 0) { 222 CLDRConverter.warning("empty pattern for " + key); 223 } 224 numberPatterns[i] = value; 225 } 226 myMap.put("NumberPatterns", numberPatterns); 227 break; 228 } 229 } 230 231 // if myMap has any of NUMBER_ELEMENT_KEYS, create a complete NumberElements. 232 String defaultScript = (String) myMap.get("DefaultNumberingSystem"); 233 @SuppressWarnings("unchecked") 234 List<String> scripts = (List<String>) myMap.get("numberingScripts"); 235 if (defaultScript == null && scripts != null) { 236 // Some locale data has no default script for numbering even with mutiple scripts. 237 // Take the first one as default in that case. 238 defaultScript = scripts.get(0); 239 myMap.put("DefaultNumberingSystem", defaultScript); 240 } 241 if (scripts != null) { 242 for (String script : scripts) { 243 for (String k : NUMBER_ELEMENT_KEYS) { 244 String[] numberElements = new String[NUMBER_ELEMENT_KEYS.length]; 245 for (int i = 0; i < NUMBER_ELEMENT_KEYS.length; i++) { 246 String key = script + "." + NUMBER_ELEMENT_KEYS[i]; 247 String value = (String) myMap.remove(key); 248 if (value == null) { 249 if (key.endsWith("/pattern")) { 250 value = "#"; 251 } else { 252 value = (String) parentsMap.get(key); 253 if (value == null) { 254 // the last resort is "latn" 255 key = "latn." + NUMBER_ELEMENT_KEYS[i]; 256 value = (String) parentsMap.get(key); 257 if (value == null) { 258 throw new InternalError("NumberElements: null for " + key); 259 } 260 } 261 } 262 } 263 numberElements[i] = value; 264 } 265 myMap.put(script + "." + "NumberElements", numberElements); 266 break; 267 } 268 } 269 } 270 271 // another hack: parentsMap is not used for date-time resources. 272 if ("root".equals(id)) { 273 parentsMap = null; 274 } 275 276 for (CalendarType calendarType : CalendarType.values()) { 277 String calendarPrefix = calendarType.keyElementName(); 278 // handle multiple inheritance for month and day names 279 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames"); 280 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations"); 281 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNarrows"); 282 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames"); 283 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations"); 284 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNarrows"); 285 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers"); 286 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "narrow.AmPmMarkers"); 287 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "abbreviated.AmPmMarkers"); 288 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNames"); 289 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterAbbreviations"); 290 handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNarrows"); 291 292 adjustEraNames(myMap, calendarType); 293 294 handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns"); 295 handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns"); 296 handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns"); 297 } 298 299 // First, weed out any empty timezone or metazone names from myMap. 300 // Fill in any missing abbreviations if locale is "en". 301 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 302 String key = it.next(); 303 if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX) 304 || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 305 @SuppressWarnings("unchecked") 306 Map<String, String> nameMap = (Map<String, String>) myMap.get(key); 307 if (nameMap.isEmpty()) { 308 // Some zones have only exemplarCity, which become empty. 309 // Remove those from the map. 310 it.remove(); 311 continue; 312 } 313 314 if (id.equals("en")) { 315 fillInJREs(key, nameMap); 316 } 317 } 318 } 319 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 320 String key = it.next(); 321 if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX) 322 || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 323 @SuppressWarnings("unchecked") 324 Map<String, String> nameMap = (Map<String, String>) myMap.get(key); 325 // Convert key/value pairs to an array. 326 String[] names = new String[ZONE_NAME_KEYS.length]; 327 int ix = 0; 328 for (String nameKey : ZONE_NAME_KEYS) { 329 String name = nameMap.get(nameKey); 330 if (name == null) { 331 @SuppressWarnings("unchecked") 332 Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key); 333 if (parentNames != null) { 334 name = parentNames.get(nameKey); 335 } 336 } 337 names[ix++] = name; 338 } 339 if (hasNulls(names)) { 340 String metaKey = toMetaZoneKey(key); 341 if (metaKey != null) { 342 Object obj = myMap.get(metaKey); 343 if (obj instanceof String[]) { 344 String[] metaNames = (String[]) obj; 345 for (int i = 0; i < names.length; i++) { 346 if (names[i] == null) { 347 names[i] = metaNames[i]; 348 } 349 } 350 } else if (obj instanceof Map) { 351 @SuppressWarnings("unchecked") 352 Map<String, String> m = (Map<String, String>) obj; 353 for (int i = 0; i < names.length; i++) { 354 if (names[i] == null) { 355 names[i] = m.get(ZONE_NAME_KEYS[i]); 356 } 357 } 358 } 359 } 360 // If there are still any nulls, try filling in them from en data. 361 if (hasNulls(names) && !id.equals("en")) { 362 @SuppressWarnings("unchecked") 363 String[] enNames = (String[]) Bundle.getBundle("en").getTargetMap().get(key); 364 if (enNames == null) { 365 if (metaKey != null) { 366 @SuppressWarnings("unchecked") 367 String[] metaNames = (String[]) Bundle.getBundle("en").getTargetMap().get(metaKey); 368 enNames = metaNames; 369 } 370 } 371 if (enNames != null) { 372 for (int i = 0; i < names.length; i++) { 373 if (names[i] == null) { 374 names[i] = enNames[i]; 375 } 376 } 377 } 378 // If there are still nulls, give up names. 379 if (hasNulls(names)) { 380 names = null; 381 } 382 } 383 } 384 // replace the Map with the array 385 if (names != null) { 386 myMap.put(key, names); 387 } else { 388 it.remove(); 389 } 390 } 391 } 392 // replace empty era names with parentMap era names 393 for (String key : ERA_KEYS) { 394 Object value = myMap.get(key); 395 if (value != null && value instanceof String[]) { 396 String[] eraStrings = (String[]) value; 397 for (String eraString : eraStrings) { 398 if (eraString == null || eraString.isEmpty()) { 399 fillInElements(parentsMap, key, value); 400 } 401 } 402 } 403 } 404 405 // Remove all duplicates 406 if (Objects.nonNull(parentsMap)) { 407 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 408 String key = it.next(); 409 if (!key.equals("numberingScripts") && // real body "NumberElements" may differ 410 Objects.deepEquals(parentsMap.get(key), myMap.get(key))) { 411 it.remove(); 412 } 413 } 414 } 415 416 targetMap = myMap; 417 return myMap; 418 } 419 420 private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) { 421 String formatKey = key + "/format"; 422 Object format = map.get(formatKey); 423 if (format != null) { 424 map.remove(formatKey); 425 map.put(key, format); 426 if (fillInElements(parents, formatKey, format)) { 427 map.remove(key); 428 } 429 } 430 String standaloneKey = key + "/stand-alone"; 431 Object standalone = map.get(standaloneKey); 432 if (standalone != null) { 433 map.remove(standaloneKey); 434 String realKey = key; 435 if (format != null) { 436 realKey = "standalone." + key; 437 } 438 map.put(realKey, standalone); 439 if (fillInElements(parents, standaloneKey, standalone)) { 440 map.remove(realKey); 441 } 442 } 443 } 444 445 /** 446 * Fills in any empty elements with its parent element. Returns true if the resulting array is 447 * identical to its parent array. 448 * 449 * @param parents 450 * @param key 451 * @param value 452 * @return true if the resulting array is identical to its parent array. 453 */ 454 private boolean fillInElements(Map<String, Object> parents, String key, Object value) { 455 if (parents == null) { 456 return false; 457 } 458 if (value instanceof String[]) { 459 Object pvalue = parents.get(key); 460 if (pvalue != null && pvalue instanceof String[]) { 461 String[] strings = (String[]) value; 462 String[] pstrings = (String[]) pvalue; 463 for (int i = 0; i < strings.length; i++) { 464 if (strings[i] == null || strings[i].length() == 0) { 465 strings[i] = pstrings[i]; 466 } 467 } 468 return Arrays.equals(strings, pstrings); 469 } 470 } 471 return false; 472 } 473 474 /* 475 * Adjusts String[] for era names because JRE's Calendars use different 476 * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars. 477 */ 478 private void adjustEraNames(Map<String, Object> map, CalendarType type) { 479 String[][] eraNames = new String[ERA_KEYS.length][]; 480 String[] realKeys = new String[ERA_KEYS.length]; 481 int index = 0; 482 for (String key : ERA_KEYS) { 483 String realKey = type.keyElementName() + key; 484 String[] value = (String[]) map.get(realKey); 485 if (value != null) { 486 switch (type) { 487 case GREGORIAN: 488 break; 489 490 case JAPANESE: 491 { 492 String[] newValue = new String[value.length + 1]; 493 String[] julianEras = (String[]) map.get(key); 494 if (julianEras != null && julianEras.length >= 2) { 495 newValue[0] = julianEras[1]; 496 } else { 497 newValue[0] = ""; 498 } 499 System.arraycopy(value, 0, newValue, 1, value.length); 500 value = newValue; 501 } 502 break; 503 504 case BUDDHIST: 505 // Replace the value 506 value = new String[] {"BC", value[0]}; 507 break; 508 509 case ISLAMIC: 510 // Replace the value 511 value = new String[] {"", value[0]}; 512 break; 513 } 514 if (!key.equals(realKey)) { 515 map.put(realKey, value); 516 } 517 } 518 realKeys[index] = realKey; 519 eraNames[index++] = value; 520 } 521 for (int i = 0; i < eraNames.length; i++) { 522 if (eraNames[i] == null) { 523 map.put(realKeys[i], null); 524 } 525 } 526 } 527 528 private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap, 529 CalendarType calendarType, String name) { 530 String calendarPrefix = calendarType.keyElementName(); 531 for (String k : patternKeys) { 532 if (myMap.containsKey(calendarPrefix + k)) { 533 int len = patternKeys.length; 534 List<String> rawPatterns = new ArrayList<>(len); 535 List<String> patterns = new ArrayList<>(len); 536 for (int i = 0; i < len; i++) { 537 String key = calendarPrefix + patternKeys[i]; 538 String pattern = (String) myMap.remove(key); 539 if (pattern == null) { 540 pattern = (String) parentsMap.remove(key); 541 } 542 rawPatterns.add(i, pattern); 543 if (pattern != null) { 544 patterns.add(i, translateDateFormatLetters(calendarType, pattern)); 545 } else { 546 patterns.add(i, null); 547 } 548 } 549 // If patterns is empty or has any nulls, discard patterns. 550 if (patterns.isEmpty()) { 551 return; 552 } 553 String key = calendarPrefix + name; 554 if (!rawPatterns.equals(patterns)) { 555 myMap.put("java.time." + key, rawPatterns.toArray(new String[len])); 556 } 557 myMap.put(key, patterns.toArray(new String[len])); 558 break; 559 } 560 } 561 } 562 563 private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) { 564 String pattern = cldrFormat; 565 int length = pattern.length(); 566 boolean inQuote = false; 567 StringBuilder jrePattern = new StringBuilder(length); 568 int count = 0; 569 char lastLetter = 0; 570 571 for (int i = 0; i < length; i++) { 572 char c = pattern.charAt(i); 573 574 if (c == '\'') { 575 // '' is treated as a single quote regardless of being 576 // in a quoted section. 577 if ((i + 1) < length) { 578 char nextc = pattern.charAt(i + 1); 579 if (nextc == '\'') { 580 i++; 581 if (count != 0) { 582 convert(calendarType, lastLetter, count, jrePattern); 583 lastLetter = 0; 584 count = 0; 585 } 586 jrePattern.append("''"); 587 continue; 588 } 589 } 590 if (!inQuote) { 591 if (count != 0) { 592 convert(calendarType, lastLetter, count, jrePattern); 593 lastLetter = 0; 594 count = 0; 595 } 596 inQuote = true; 597 } else { 598 inQuote = false; 599 } 600 jrePattern.append(c); 601 continue; 602 } 603 if (inQuote) { 604 jrePattern.append(c); 605 continue; 606 } 607 if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { 608 if (count != 0) { 609 convert(calendarType, lastLetter, count, jrePattern); 610 lastLetter = 0; 611 count = 0; 612 } 613 jrePattern.append(c); 614 continue; 615 } 616 617 if (lastLetter == 0 || lastLetter == c) { 618 lastLetter = c; 619 count++; 620 continue; 621 } 622 convert(calendarType, lastLetter, count, jrePattern); 623 lastLetter = c; 624 count = 1; 625 } 626 627 if (inQuote) { 628 throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat); 629 } 630 631 if (count != 0) { 632 convert(calendarType, lastLetter, count, jrePattern); 633 } 634 if (cldrFormat.contentEquals(jrePattern)) { 635 return cldrFormat; 636 } 637 return jrePattern.toString(); 638 } 639 640 private String toMetaZoneKey(String tzKey) { 641 if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) { 642 String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 643 String meta = CLDRConverter.handlerMetaZones.get(tz); 644 if (meta != null) { 645 return CLDRConverter.METAZONE_ID_PREFIX + meta; 646 } 647 } 648 return null; 649 } 650 651 static List<Object[]> jreTimeZoneNames = Arrays.asList(TimeZoneNames.getContents()); 652 private void fillInJREs(String key, Map<String, String> map) { 653 String tzid = null; 654 655 if (key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 656 // Look for tzid 657 String meta = key.substring(CLDRConverter.METAZONE_ID_PREFIX.length()); 658 if (meta.equals("GMT")) { 659 tzid = meta; 660 } else { 661 for (String tz : CLDRConverter.handlerMetaZones.keySet()) { 662 if (CLDRConverter.handlerMetaZones.get(tz).equals(meta)) { 663 tzid = tz; 664 break; 665 } 666 } 667 } 668 } else { 669 tzid = key.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 670 } 671 672 if (tzid != null) { 673 for (Object[] jreZone : jreTimeZoneNames) { 674 if (jreZone[0].equals(tzid)) { 675 for (int i = 0; i < ZONE_NAME_KEYS.length; i++) { 676 if (map.get(ZONE_NAME_KEYS[i]) == null) { 677 String[] jreNames = (String[])jreZone[1]; 678 map.put(ZONE_NAME_KEYS[i], jreNames[i]); 679 } 680 } 681 break; 682 } 683 } 684 } 685 } 686 687 private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { 688 switch (cldrLetter) { 689 case 'G': 690 if (calendarType != CalendarType.GREGORIAN) { 691 // Adjust the number of 'G's for JRE SimpleDateFormat 692 if (count == 5) { 693 // CLDR narrow -> JRE short 694 count = 1; 695 } else if (count == 1) { 696 // CLDR abbr -> JRE long 697 count = 4; 698 } 699 } 700 appendN(cldrLetter, count, sb); 701 break; 702 703 // TODO: support 'c' and 'e' in JRE SimpleDateFormat 704 // Use 'u' and 'E' for now. 705 case 'c': 706 case 'e': 707 switch (count) { 708 case 1: 709 sb.append('u'); 710 break; 711 case 3: 712 case 4: 713 appendN('E', count, sb); 714 break; 715 case 5: 716 appendN('E', 3, sb); 717 break; 718 } 719 break; 720 721 case 'l': 722 // 'l' is deprecated as a pattern character. Should be ignored. 723 break; 724 725 case 'u': 726 // Use 'y' for now. 727 appendN('y', count, sb); 728 break; 729 730 case 'v': 731 case 'V': 732 appendN('z', count, sb); 733 break; 734 735 case 'Z': 736 if (count == 4 || count == 5) { 737 sb.append("XXX"); 738 } 739 break; 740 741 case 'U': 742 case 'q': 743 case 'Q': 744 case 'g': 745 case 'j': 746 case 'A': 747 throw new InternalError(String.format("Unsupported letter: '%c', count=%d, id=%s%n", 748 cldrLetter, count, id)); 749 default: 750 appendN(cldrLetter, count, sb); 751 break; 752 } 753 } 754 755 private void appendN(char c, int n, StringBuilder sb) { 756 for (int i = 0; i < n; i++) { 757 sb.append(c); 758 } 759 } 760 761 private static boolean hasNulls(Object[] array) { 762 for (int i = 0; i < array.length; i++) { 763 if (array[i] == null) { 764 return true; 765 } 766 } 767 return false; 768 } 769 }