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