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 formatMapKey = key + "/format"; 395 Object format = map.get(formatMapKey); 396 if (format != null) { 397 map.remove(formatMapKey); 398 map.put(key, format); 399 if (fillInElements(parents, formatMapKey, format)) { 400 map.remove(key); 401 } 402 } 403 String standaloneMapKey = key + "/stand-alone"; 404 Object standalone = map.get(standaloneMapKey); 405 if (standalone != null) { 406 map.remove(standaloneMapKey); 407 String standaloneResourceKey = "standalone." + key; 408 map.put(standaloneResourceKey, standalone); 409 if (fillInElements(parents, standaloneMapKey, standalone)) { 410 map.remove(standaloneResourceKey); 411 } 412 } 413 } 414 415 /** 416 * Fills in any empty elements with its parent element. Returns true if the resulting array is 417 * identical to its parent array. 418 * 419 * @param parents 420 * @param key 421 * @param value 422 * @return true if the resulting array is identical to its parent array. 423 */ 424 private boolean fillInElements(Map<String, Object> parents, String key, Object value) { 425 if (parents == null) { 426 return false; 427 } 428 if (value instanceof String[]) { 429 Object pvalue = parents.get(key); 430 if (pvalue != null && pvalue instanceof String[]) { 431 String[] strings = (String[]) value; 432 String[] pstrings = (String[]) pvalue; 433 for (int i = 0; i < strings.length; i++) { 434 if (strings[i] == null || strings[i].length() == 0) { 435 strings[i] = pstrings[i]; 436 } 437 } 438 return Arrays.equals(strings, pstrings); 439 } 440 } 441 return false; 442 } 443 444 /* 445 * Adjusts String[] for era names because JRE's Calendars use different 446 * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars. 447 */ 448 private void adjustEraNames(Map<String, Object> map, CalendarType type) { 449 String[][] eraNames = new String[ERA_KEYS.length][]; 450 String[] realKeys = new String[ERA_KEYS.length]; 451 int index = 0; 452 for (String key : ERA_KEYS) { 453 String realKey = type.keyElementName() + key; 454 String[] value = (String[]) map.get(realKey); 455 if (value != null) { 456 switch (type) { 457 case GREGORIAN: 458 break; 459 460 case JAPANESE: 461 { 462 String[] newValue = new String[value.length + 1]; 463 String[] julianEras = (String[]) map.get(key); 464 if (julianEras != null && julianEras.length >= 2) { 465 newValue[0] = julianEras[1]; 466 } else { 467 newValue[0] = ""; 468 } 469 System.arraycopy(value, 0, newValue, 1, value.length); 470 value = newValue; 471 472 // fix up 'Reiwa' era, which can be missing in some locales 473 if (value[value.length - 1] == null) { 474 value[value.length - 1] = (key.startsWith("narrow.") ? "R" : "Reiwa"); 475 } 476 } 477 break; 478 479 case BUDDHIST: 480 // Replace the value 481 value = new String[] {"BC", value[0]}; 482 break; 483 484 case ISLAMIC: 485 // Replace the value 486 value = new String[] {"", value[0]}; 487 break; 488 } 489 if (!key.equals(realKey)) { 490 map.put(realKey, value); 491 map.put("java.time." + realKey, value); 492 } 493 } 494 realKeys[index] = realKey; 495 eraNames[index++] = value; 496 } 497 for (int i = 0; i < eraNames.length; i++) { 498 if (eraNames[i] == null) { 499 map.put(realKeys[i], null); 500 } 501 } 502 } 503 504 private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap, 505 CalendarType calendarType, String name) { 506 String calendarPrefix = calendarType.keyElementName(); 507 for (String k : patternKeys) { 508 if (myMap.containsKey(calendarPrefix + k)) { 509 int len = patternKeys.length; 510 List<String> dateTimePatterns = new ArrayList<>(len); 511 List<String> sdfPatterns = new ArrayList<>(len); 512 for (int i = 0; i < len; i++) { 513 String key = calendarPrefix + patternKeys[i]; 514 String pattern = (String) myMap.remove(key); 515 if (pattern == null) { 516 pattern = (String) parentsMap.remove(key); 517 } 518 if (pattern != null) { 519 // Perform date-time format pattern conversion which is 520 // applicable to both SimpleDateFormat and j.t.f.DateTimeFormatter. 521 // For example, character 'B' is mapped with 'a', as 'B' is not 522 // supported in either SimpleDateFormat or j.t.f.DateTimeFormatter 523 String transPattern = translateDateFormatLetters(calendarType, pattern, this::convertDateTimePatternLetter); 524 dateTimePatterns.add(i, transPattern); 525 // Additionally, perform SDF specific date-time format pattern conversion 526 sdfPatterns.add(i, translateDateFormatLetters(calendarType, transPattern, this::convertSDFLetter)); 527 } else { 528 dateTimePatterns.add(i, null); 529 sdfPatterns.add(i, null); 530 } 531 } 532 // If empty, discard patterns 533 if (sdfPatterns.isEmpty()) { 534 return; 535 } 536 String key = calendarPrefix + name; 537 538 // If additional changes are made in the SDF specific conversion, 539 // keep the commonly converted patterns as java.time patterns 540 if (!dateTimePatterns.equals(sdfPatterns)) { 541 myMap.put("java.time." + key, dateTimePatterns.toArray(String[]::new)); 542 } 543 myMap.put(key, sdfPatterns.toArray(new String[len])); 544 break; 545 } 546 } 547 } 548 549 private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat, ConvertDateTimeLetters converter) { 550 String pattern = cldrFormat; 551 int length = pattern.length(); 552 boolean inQuote = false; 553 StringBuilder jrePattern = new StringBuilder(length); 554 int count = 0; 555 char lastLetter = 0; 556 557 for (int i = 0; i < length; i++) { 558 char c = pattern.charAt(i); 559 560 if (c == '\'') { 561 // '' is treated as a single quote regardless of being 562 // in a quoted section. 563 if ((i + 1) < length) { 564 char nextc = pattern.charAt(i + 1); 565 if (nextc == '\'') { 566 i++; 567 if (count != 0) { 568 converter.convert(calendarType, lastLetter, count, jrePattern); 569 lastLetter = 0; 570 count = 0; 571 } 572 jrePattern.append("''"); 573 continue; 574 } 575 } 576 if (!inQuote) { 577 if (count != 0) { 578 converter.convert(calendarType, lastLetter, count, jrePattern); 579 lastLetter = 0; 580 count = 0; 581 } 582 inQuote = true; 583 } else { 584 inQuote = false; 585 } 586 jrePattern.append(c); 587 continue; 588 } 589 if (inQuote) { 590 jrePattern.append(c); 591 continue; 592 } 593 if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { 594 if (count != 0) { 595 converter.convert(calendarType, lastLetter, count, jrePattern); 596 lastLetter = 0; 597 count = 0; 598 } 599 jrePattern.append(c); 600 continue; 601 } 602 603 if (lastLetter == 0 || lastLetter == c) { 604 lastLetter = c; 605 count++; 606 continue; 607 } 608 converter.convert(calendarType, lastLetter, count, jrePattern); 609 lastLetter = c; 610 count = 1; 611 } 612 613 if (inQuote) { 614 throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat); 615 } 616 617 if (count != 0) { 618 converter.convert(calendarType, lastLetter, count, jrePattern); 619 } 620 if (cldrFormat.contentEquals(jrePattern)) { 621 return cldrFormat; 622 } 623 return jrePattern.toString(); 624 } 625 626 private String toMetaZoneKey(String tzKey) { 627 if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) { 628 String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 629 String meta = CLDRConverter.handlerMetaZones.get(tz); 630 if (meta != null) { 631 return CLDRConverter.METAZONE_ID_PREFIX + meta; 632 } 633 } 634 return null; 635 } 636 637 static List<Object[]> jreTimeZoneNames = Arrays.asList(TimeZoneNames.getContents()); 638 private void fillInJREs(String key, Map<String, String> map) { 639 String tzid = null; 640 641 if (key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { 642 // Look for tzid 643 String meta = key.substring(CLDRConverter.METAZONE_ID_PREFIX.length()); 644 if (meta.equals("GMT")) { 645 tzid = meta; 646 } else { 647 for (String tz : CLDRConverter.handlerMetaZones.keySet()) { 648 if (CLDRConverter.handlerMetaZones.get(tz).equals(meta)) { 649 tzid = tz; 650 break; 651 } 652 } 653 } 654 } else { 655 tzid = key.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); 656 } 657 658 if (tzid != null) { 659 for (Object[] jreZone : jreTimeZoneNames) { 660 if (jreZone[0].equals(tzid)) { 661 for (int i = 0; i < ZONE_NAME_KEYS.length; i++) { 662 if (map.get(ZONE_NAME_KEYS[i]) == null) { 663 String[] jreNames = (String[])jreZone[1]; 664 map.put(ZONE_NAME_KEYS[i], jreNames[i]); 665 } 666 } 667 break; 668 } 669 } 670 } 671 } 672 673 /** 674 * Perform a generic conversion of CLDR date-time format pattern letter based 675 * on the support given by the SimpleDateFormat and the j.t.f.DateTimeFormatter 676 * for date-time formatting. 677 */ 678 private void convertDateTimePatternLetter(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { 679 switch (cldrLetter) { 680 case 'u': 681 // Change cldr letter 'u' to 'y', as 'u' is interpreted as 682 // "Extended year (numeric)" in CLDR/LDML, 683 // which is not supported in SimpleDateFormat and 684 // j.t.f.DateTimeFormatter, so it is replaced with 'y' 685 // as the best approximation 686 appendN('y', count, sb); 687 break; 688 case 'B': 689 // 'B' character (day period) is not supported by 690 // SimpleDateFormat and j.t.f.DateTimeFormatter, 691 // this is a workaround in which 'B' character 692 // appearing in CLDR date-time pattern is replaced 693 // with 'a' character and hence resolved with am/pm strings. 694 // This workaround is based on the the fallback mechanism 695 // specified in LDML spec for 'B' character, when a locale 696 // does not have data for day period ('B') 697 appendN('a', count, sb); 698 break; 699 default: 700 appendN(cldrLetter, count, sb); 701 break; 702 703 } 704 } 705 706 /** 707 * Perform a conversion of CLDR date-time format pattern letter which is 708 * specific to the SimpleDateFormat. 709 */ 710 private void convertSDFLetter(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { 711 switch (cldrLetter) { 712 case 'G': 713 if (calendarType != CalendarType.GREGORIAN) { 714 // Adjust the number of 'G's for JRE SimpleDateFormat 715 if (count == 5) { 716 // CLDR narrow -> JRE short 717 count = 1; 718 } else if (count == 1) { 719 // CLDR abbr -> JRE long 720 count = 4; 721 } 722 } 723 appendN(cldrLetter, count, sb); 724 break; 725 726 // TODO: support 'c' and 'e' in JRE SimpleDateFormat 727 // Use 'u' and 'E' for now. 728 case 'c': 729 case 'e': 730 switch (count) { 731 case 1: 732 sb.append('u'); 733 break; 734 case 3: 735 case 4: 736 appendN('E', count, sb); 737 break; 738 case 5: 739 appendN('E', 3, sb); 740 break; 741 } 742 break; 743 744 case 'v': 745 case 'V': 746 appendN('z', count, sb); 747 break; 748 749 case 'Z': 750 if (count == 4 || count == 5) { 751 sb.append("XXX"); 752 } 753 break; 754 755 default: 756 appendN(cldrLetter, count, sb); 757 break; 758 } 759 } 760 761 private void appendN(char c, int n, StringBuilder sb) { 762 for (int i = 0; i < n; i++) { 763 sb.append(c); 764 } 765 } 766 767 private static boolean hasNulls(Object[] array) { 768 for (int i = 0; i < array.length; i++) { 769 if (array[i] == null) { 770 return true; 771 } 772 } 773 return false; 774 } 775 776 @FunctionalInterface 777 private interface ConvertDateTimeLetters { 778 void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb); 779 } 780 781 /** 782 * Returns a complete string array for NumberElements or NumberPatterns. If any 783 * array element is missing, it will fall back to parents map, as well as 784 * numbering script fallback. 785 */ 786 private String[] createNumberArray(Map<String, Object> myMap, Map<String, Object>parentsMap, 787 String[] keys, String script) { 788 String[] numArray = new String[keys.length]; 789 for (int i = 0; i < keys.length; i++) { 790 String key = script + "." + keys[i]; 791 final int idx = i; 792 Optional.ofNullable( 793 myMap.getOrDefault(key, 794 // if value not found in myMap, search for parentsMap 795 parentsMap.getOrDefault(key, 796 parentsMap.getOrDefault(keys[i], 797 // the last resort is "latn" 798 parentsMap.get("latn." + keys[i]))))) 799 .ifPresentOrElse(v -> numArray[idx] = (String)v, () -> { 800 if (keys == NUMBER_PATTERN_KEYS) { 801 // NumberPatterns 802 if (!key.endsWith("accounting")) { 803 // throw error unless it is for "accounting", 804 // which may be missing. 805 throw new InternalError("NumberPatterns: null for " + 806 key + ", id: " + id); 807 } 808 } else { 809 // NumberElements 810 assert keys == NUMBER_ELEMENT_KEYS; 811 if (key.endsWith("/pattern")) { 812 numArray[idx] = "#"; 813 } else { 814 throw new InternalError("NumberElements: null for " + 815 key + ", id: " + id); 816 } 817 }}); 818 } 819 return numArray; 820 } 821 }