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[] 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 326 // Convert key/value pairs to an array. 327 String[] names = new String[ZONE_NAME_KEYS.length]; 328 int ix = 0; 329 for (String nameKey : ZONE_NAME_KEYS) { 330 String name = nameMap.get(nameKey); 331 if (name == null && parentsMap != null) { 332 @SuppressWarnings("unchecked") 333 Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key); 334 if (parentNames != null) { 335 name = parentNames.get(nameKey); 336 } 337 } 338 names[ix++] = name; 339 } 340 if (hasNulls(names)) { 341 String metaKey = toMetaZoneKey(key); 342 if (metaKey != null) { 343 Object obj = myMap.get(metaKey); 344 if (obj instanceof String[]) { 345 String[] metaNames = (String[]) obj; 346 for (int i = 0; i < names.length; i++) { 347 if (names[i] == null) { 348 names[i] = metaNames[i]; 349 } 350 } 351 } else if (obj instanceof Map) { 352 @SuppressWarnings("unchecked") 353 Map<String, String> m = (Map<String, String>) obj; 354 for (int i = 0; i < names.length; i++) { 355 if (names[i] == null) { 356 names[i] = m.get(ZONE_NAME_KEYS[i]); 357 } 358 } 359 } 360 } 361 } 362 // replace the Map with the array 363 if (names != null) { 364 myMap.put(key, names); 365 } else { 366 it.remove(); 367 } 368 } 369 } 370 // replace empty era names with parentMap era names 371 for (String key : ERA_KEYS) { 372 Object value = myMap.get(key); 373 if (value != null && value instanceof String[]) { 374 String[] eraStrings = (String[]) value; 375 for (String eraString : eraStrings) { 376 if (eraString == null || eraString.isEmpty()) { 377 fillInElements(parentsMap, key, value); 378 } 379 } 380 } 381 } 382 383 // Remove all duplicates 384 if (Objects.nonNull(parentsMap)) { 385 for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { 386 String key = it.next(); 387 if (!key.equals("numberingScripts") && // real body "NumberElements" may differ 388 Objects.deepEquals(parentsMap.get(key), myMap.get(key))) { 389 it.remove(); 390 } 391 } 392 } 393 394 targetMap = myMap; 395 return myMap; 396 } 397 398 private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) { 399 String formatKey = key + "/format"; 400 Object format = map.get(formatKey); 401 if (format != null) { 402 map.remove(formatKey); 403 map.put(key, format); 404 if (fillInElements(parents, formatKey, format)) { 405 map.remove(key); 406 } 407 } 408 String standaloneKey = key + "/stand-alone"; 409 Object standalone = map.get(standaloneKey); 410 if (standalone != null) { 411 map.remove(standaloneKey); 412 String realKey = key; 413 if (format != null) { 414 realKey = "standalone." + key; 415 } 416 map.put(realKey, standalone); 417 if (fillInElements(parents, standaloneKey, standalone)) { 418 map.remove(realKey); 419 } 420 } 421 } 422 423 /** 424 * Fills in any empty elements with its parent element. Returns true if the resulting array is 425 * identical to its parent array. 426 * 427 * @param parents 428 * @param key 429 * @param value 430 * @return true if the resulting array is identical to its parent array. 431 */ 432 private boolean fillInElements(Map<String, Object> parents, String key, Object value) { 433 if (parents == null) { 434 return false; 435 } 436 if (value instanceof String[]) { 437 Object pvalue = parents.get(key); 438 if (pvalue != null && pvalue instanceof String[]) { 439 String[] strings = (String[]) value; 440 String[] pstrings = (String[]) pvalue; 441 for (int i = 0; i < strings.length; i++) { 442 if (strings[i] == null || strings[i].length() == 0) { 443 strings[i] = pstrings[i]; 444 } 445 } 446 return Arrays.equals(strings, pstrings); 447 } 448 } 449 return false; 450 } 451 452 /* 453 * Adjusts String[] for era names because JRE's Calendars use different 454 * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars. 455 */ 456 private void adjustEraNames(Map<String, Object> map, CalendarType type) { 457 String[][] eraNames = new String[ERA_KEYS.length][]; 458 String[] realKeys = new String[ERA_KEYS.length]; 459 int index = 0; 460 for (String key : ERA_KEYS) { 461 String realKey = type.keyElementName() + key; 462 String[] value = (String[]) map.get(realKey); 463 if (value != null) { 464 switch (type) { 465 case GREGORIAN: 466 break; 467 468 case JAPANESE: 469 { 470 String[] newValue = new String[value.length + 1]; 471 String[] julianEras = (String[]) map.get(key); 472 if (julianEras != null && julianEras.length >= 2) { 473 newValue[0] = julianEras[1]; 474 } else { 475 newValue[0] = ""; 476 } 477 System.arraycopy(value, 0, newValue, 1, value.length); 478 value = newValue; 479 } 480 break; 481 482 case BUDDHIST: 483 // Replace the value 484 value = new String[] {"BC", value[0]}; 485 break; 486 487 case ISLAMIC: 488 // Replace the value 489 value = new String[] {"", value[0]}; 490 break; 491 } 492 if (!key.equals(realKey)) { 493 map.put(realKey, value); 494 } 495 } 496 realKeys[index] = realKey; 497 eraNames[index++] = value; 498 } 499 for (int i = 0; i < eraNames.length; i++) { 500 if (eraNames[i] == null) { 501 map.put(realKeys[i], null); 502 } 503 } 504 } 505 506 private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap, 507 CalendarType calendarType, String name) { 508 String calendarPrefix = calendarType.keyElementName(); 509 for (String k : patternKeys) { 510 if (myMap.containsKey(calendarPrefix + k)) { 511 int len = patternKeys.length; 512 List<String> rawPatterns = new ArrayList<>(len); 513 List<String> patterns = new ArrayList<>(len); 514 for (int i = 0; i < len; i++) { 515 String key = calendarPrefix + patternKeys[i]; 516 String pattern = (String) myMap.remove(key); 517 if (pattern == null) { 518 pattern = (String) parentsMap.remove(key); 519 } 520 rawPatterns.add(i, pattern); 521 if (pattern != null) { 522 patterns.add(i, translateDateFormatLetters(calendarType, pattern)); 523 } else { 524 patterns.add(i, null); 525 } 526 } 527 // If patterns is empty or has any nulls, discard patterns. 528 if (patterns.isEmpty()) { 529 return; 530 } 531 String key = calendarPrefix + name; 532 if (!rawPatterns.equals(patterns)) { 533 myMap.put("java.time." + key, rawPatterns.toArray(new String[len])); 534 } 535 myMap.put(key, patterns.toArray(new String[len])); 536 break; 537 } 538 } 539 } 540 541 private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) { 542 String pattern = cldrFormat; 543 int length = pattern.length(); 544 boolean inQuote = false; 545 StringBuilder jrePattern = new StringBuilder(length); 546 int count = 0; 547 char lastLetter = 0; 548 549 for (int i = 0; i < length; i++) { 550 char c = pattern.charAt(i); 551 552 if (c == '\'') { 553 // '' is treated as a single quote regardless of being 554 // in a quoted section. 555 if ((i + 1) < length) { 556 char nextc = pattern.charAt(i + 1); 557 if (nextc == '\'') { 558 i++; 559 if (count != 0) { 560 convert(calendarType, lastLetter, count, jrePattern); 561 lastLetter = 0; 562 count = 0; 563 } 564 jrePattern.append("''"); 565 continue; 566 } 567 } 568 if (!inQuote) { 569 if (count != 0) { 570 convert(calendarType, lastLetter, count, jrePattern); 571 lastLetter = 0; 572 count = 0; 573 } 574 inQuote = true; 575 } else { 576 inQuote = false; 577 } 578 jrePattern.append(c); 579 continue; 580 } 581 if (inQuote) { 582 jrePattern.append(c); 583 continue; 584 } 585 if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { 586 if (count != 0) { 587 convert(calendarType, lastLetter, count, jrePattern); 588 lastLetter = 0; 589 count = 0; 590 } 591 jrePattern.append(c); 592 continue; 593 } 594 595 if (lastLetter == 0 || lastLetter == c) { 596 lastLetter = c; 597 count++; 598 continue; 599 } 600 convert(calendarType, lastLetter, count, jrePattern); 601 lastLetter = c; 602 count = 1; 603 } 604 605 if (inQuote) { 606 throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat); 607 } 608 609 if (count != 0) { 610 convert(calendarType, lastLetter, count, jrePattern); 611 } 612 if (cldrFormat.contentEquals(jrePattern)) { 613 return cldrFormat; 614 } 615 return jrePattern.toString(); 616 } 617 618 private String toMetaZoneKey(String tzKey) { 619 if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) { 620 String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 621 String meta = CLDRConverter.handlerMetaZones.get(tz); 622 if (meta != null) { 623 return CLDRConverter.METAZONE_ID_PREFIX + meta; 624 } 625 } 626 return null; 627 } 628 629 static List<Object[]> jreTimeZoneNames = Arrays.asList(TimeZoneNames.getContents()); 630 private void fillInJREs(String key, Map<String, String> map) { 631 String tzid = null; 632 633 if (key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 634 // Look for tzid 635 String meta = key.substring(CLDRConverter.METAZONE_ID_PREFIX.length()); 636 if (meta.equals("GMT")) { 637 tzid = meta; 638 } else { 639 for (String tz : CLDRConverter.handlerMetaZones.keySet()) { 640 if (CLDRConverter.handlerMetaZones.get(tz).equals(meta)) { 641 tzid = tz; 642 break; 643 } 644 } 645 } 646 } else { 647 tzid = key.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 648 } 649 650 if (tzid != null) { 651 for (Object[] jreZone : jreTimeZoneNames) { 652 if (jreZone[0].equals(tzid)) { 653 for (int i = 0; i < ZONE_NAME_KEYS.length; i++) { 654 if (map.get(ZONE_NAME_KEYS[i]) == null) { 655 String[] jreNames = (String[])jreZone[1]; 656 map.put(ZONE_NAME_KEYS[i], jreNames[i]); 657 } 658 } 659 break; 660 } 661 } 662 } 663 } 664 665 private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { 666 switch (cldrLetter) { 667 case 'G': 668 if (calendarType != CalendarType.GREGORIAN) { 669 // Adjust the number of 'G's for JRE SimpleDateFormat 670 if (count == 5) { 671 // CLDR narrow -> JRE short 672 count = 1; 673 } else if (count == 1) { 674 // CLDR abbr -> JRE long 675 count = 4; 676 } 677 } 678 appendN(cldrLetter, count, sb); 679 break; 680 681 // TODO: support 'c' and 'e' in JRE SimpleDateFormat 682 // Use 'u' and 'E' for now. 683 case 'c': 684 case 'e': 685 switch (count) { 686 case 1: 687 sb.append('u'); 688 break; 689 case 3: 690 case 4: 691 appendN('E', count, sb); 692 break; 693 case 5: 694 appendN('E', 3, sb); 695 break; 696 } 697 break; 698 699 case 'l': 700 // 'l' is deprecated as a pattern character. Should be ignored. 701 break; 702 703 case 'u': 704 // Use 'y' for now. 705 appendN('y', count, sb); 706 break; 707 708 case 'v': 709 case 'V': 710 appendN('z', count, sb); 711 break; 712 713 case 'Z': 714 if (count == 4 || count == 5) { 715 sb.append("XXX"); 716 } 717 break; 718 719 case 'U': 720 case 'q': 721 case 'Q': 722 case 'g': 723 case 'j': 724 case 'A': 725 throw new InternalError(String.format("Unsupported letter: '%c', count=%d, id=%s%n", 726 cldrLetter, count, id)); 727 default: 728 appendN(cldrLetter, count, sb); 729 break; 730 } 731 } 732 733 private void appendN(char c, int n, StringBuilder sb) { 734 for (int i = 0; i < n; i++) { 735 sb.append(c); 736 } 737 } 738 739 private static boolean hasNulls(Object[] array) { 740 for (int i = 0; i < array.length; i++) { 741 if (array[i] == null) { 742 return true; 743 } 744 } 745 return false; 746 } 747 }