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