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