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