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