| /* |
| * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package build.tools.cldrconverter; |
| |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| |
| class Bundle { |
| static enum Type { |
| LOCALENAMES, CURRENCYNAMES, TIMEZONENAMES, CALENDARDATA, FORMATDATA; |
| |
| static EnumSet<Type> ALL_TYPES = EnumSet.of(LOCALENAMES, |
| CURRENCYNAMES, |
| TIMEZONENAMES, |
| CALENDARDATA, |
| FORMATDATA); |
| } |
| |
| private static final Map<String, Bundle> bundles = new HashMap<>(); |
| |
| private static final String[] NUMBER_PATTERN_KEYS = { |
| "NumberPatterns/decimal", |
| "NumberPatterns/currency", |
| "NumberPatterns/percent", |
| "NumberPatterns/accounting" |
| }; |
| |
| private static final String[] COMPACT_NUMBER_PATTERN_KEYS = { |
| "short.CompactNumberPatterns", |
| "long.CompactNumberPatterns" |
| }; |
| |
| private static final String[] NUMBER_ELEMENT_KEYS = { |
| "NumberElements/decimal", |
| "NumberElements/group", |
| "NumberElements/list", |
| "NumberElements/percent", |
| "NumberElements/zero", |
| "NumberElements/pattern", |
| "NumberElements/minus", |
| "NumberElements/exponential", |
| "NumberElements/permille", |
| "NumberElements/infinity", |
| "NumberElements/nan", |
| "NumberElements/currencyDecimal", |
| "NumberElements/currencyGroup", |
| }; |
| |
| private static final String[] TIME_PATTERN_KEYS = { |
| "DateTimePatterns/full-time", |
| "DateTimePatterns/long-time", |
| "DateTimePatterns/medium-time", |
| "DateTimePatterns/short-time", |
| }; |
| |
| private static final String[] DATE_PATTERN_KEYS = { |
| "DateTimePatterns/full-date", |
| "DateTimePatterns/long-date", |
| "DateTimePatterns/medium-date", |
| "DateTimePatterns/short-date", |
| }; |
| |
| private static final String[] DATETIME_PATTERN_KEYS = { |
| "DateTimePatterns/full-dateTime", |
| "DateTimePatterns/long-dateTime", |
| "DateTimePatterns/medium-dateTime", |
| "DateTimePatterns/short-dateTime", |
| }; |
| |
| private static final String[] ERA_KEYS = { |
| "long.Eras", |
| "Eras", |
| "narrow.Eras" |
| }; |
| |
| static final String[] LIST_PATTERN_KEYS = { |
| "ListPatterns_standard", |
| "ListPatterns_or", |
| "ListPatterns_unit", |
| }; |
| |
| // DateFormatItem prefix |
| static final String DATEFORMATITEM_KEY_PREFIX = "DateFormatItem."; |
| static final String DATEFORMATITEM_INPUT_REGIONS_PREFIX = "DateFormatItemInputRegions."; |
| |
| // Keys for individual time zone names |
| private static final String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long"; |
| private static final String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short"; |
| private static final String TZ_STD_LONG_KEY = "timezone.displayname.standard.long"; |
| private static final String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short"; |
| private static final String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long"; |
| private static final String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short"; |
| private static final String[] ZONE_NAME_KEYS = { |
| TZ_STD_LONG_KEY, |
| TZ_STD_SHORT_KEY, |
| TZ_DST_LONG_KEY, |
| TZ_DST_SHORT_KEY, |
| TZ_GEN_LONG_KEY, |
| TZ_GEN_SHORT_KEY |
| }; |
| |
| private final String id; |
| private final String cldrPath; |
| private final EnumSet<Type> bundleTypes; |
| private final String currencies; |
| private Map<String, Object> targetMap; |
| |
| static Bundle getBundle(String id) { |
| return bundles.get(id); |
| } |
| |
| @SuppressWarnings("ConvertToStringSwitch") |
| Bundle(String id, String cldrPath, String bundles, String currencies) { |
| this.id = id; |
| this.cldrPath = cldrPath; |
| if ("localenames".equals(bundles)) { |
| bundleTypes = EnumSet.of(Type.LOCALENAMES); |
| } else if ("currencynames".equals(bundles)) { |
| bundleTypes = EnumSet.of(Type.CURRENCYNAMES); |
| } else { |
| bundleTypes = Type.ALL_TYPES; |
| } |
| if (currencies == null) { |
| currencies = "local"; |
| } |
| this.currencies = currencies; |
| addBundle(); |
| } |
| |
| private void addBundle() { |
| Bundle.bundles.put(id, this); |
| } |
| |
| String getID() { |
| return id; |
| } |
| |
| boolean isRoot() { |
| return "root".equals(id); |
| } |
| |
| String getCLDRPath() { |
| return cldrPath; |
| } |
| |
| EnumSet<Type> getBundleTypes() { |
| return bundleTypes; |
| } |
| |
| String getCurrencies() { |
| return currencies; |
| } |
| |
| /** |
| * Generate a map that contains all the data that should be |
| * visible for the bundle's locale |
| */ |
| Map<String, Object> getTargetMap() throws Exception { |
| if (targetMap != null) { |
| return targetMap; |
| } |
| |
| String[] cldrBundles = getCLDRPath().split(","); |
| |
| // myMap contains resources for id. |
| Map<String, Object> myMap = new HashMap<>(); |
| int index; |
| for (index = 0; index < cldrBundles.length; index++) { |
| if (cldrBundles[index].equals(id)) { |
| myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index])); |
| break; |
| } |
| } |
| |
| // parentsMap contains resources from id's parents. |
| Map<String, Object> parentsMap = new HashMap<>(); |
| for (int i = cldrBundles.length - 1; i > index; i--) { |
| parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i])); |
| } |
| // Duplicate myMap as parentsMap for "root" so that the |
| // fallback works. This is a hack, though. |
| if ("root".equals(cldrBundles[0])) { |
| assert parentsMap.isEmpty(); |
| parentsMap.putAll(myMap); |
| } |
| |
| // merge individual strings into arrays |
| |
| // if myMap has any of the NumberPatterns/NumberElements members, create a |
| // complete array of patterns/elements. |
| @SuppressWarnings("unchecked") |
| List<String> scripts = (List<String>) myMap.get("numberingScripts"); |
| if (scripts != null) { |
| for (String script : scripts) { |
| myMap.put(script + ".NumberPatterns", |
| createNumberArray(myMap, parentsMap, NUMBER_PATTERN_KEYS, script)); |
| myMap.put(script + ".NumberElements", |
| createNumberArray(myMap, parentsMap, NUMBER_ELEMENT_KEYS, script)); |
| } |
| } |
| |
| for (String k : COMPACT_NUMBER_PATTERN_KEYS) { |
| @SuppressWarnings("unchecked") |
| List<String> patterns = (List<String>) myMap.remove(k); |
| if (patterns != null) { |
| // Convert the map value from List<String> to String[], replacing any missing |
| // entry from the parents map, if any. |
| @SuppressWarnings("unchecked") |
| final List<String> pList = (List<String>)parentsMap.get(k); |
| int size = patterns.size(); |
| int psize = pList != null ? pList.size() : 0; |
| String[] arrPatterns = IntStream.range(0, Math.max(size, psize)) |
| .mapToObj(i -> { |
| String pattern; |
| // first try itself. |
| if (i < size) { |
| pattern = patterns.get(i); |
| if (!pattern.isEmpty()) { |
| return "{" + pattern + "}"; |
| } |
| } |
| // if not found, try parent |
| if (i < psize) { |
| pattern = pList.get(i); |
| if (!pattern.isEmpty()) { |
| return "{" + pattern + "}"; |
| } |
| } |
| // bail out with empty string |
| return ""; |
| }) |
| .toArray(String[]::new); |
| myMap.put(k, arrPatterns); |
| } |
| } |
| |
| // Processes aliases here |
| CLDRConverter.handleAliases(myMap); |
| |
| // another hack: parentsMap is not used for date-time resources. |
| if (isRoot()) { |
| parentsMap = null; |
| } |
| |
| for (CalendarType calendarType : CalendarType.values()) { |
| String calendarPrefix = calendarType.keyElementName(); |
| // handle multiple inheritance for month and day names |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNarrows"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNarrows"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "narrow.AmPmMarkers"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "abbreviated.AmPmMarkers"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNames"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterAbbreviations"); |
| handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNarrows"); |
| |
| adjustEraNames(myMap, parentsMap, calendarType); |
| |
| handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns"); |
| handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns"); |
| handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns"); |
| |
| // Skeleton |
| handleSkeletonPatterns(myMap, calendarType); |
| } |
| |
| // Skeleton input regions |
| if (isRoot()) { |
| skeletonInputRegions(myMap); |
| } |
| |
| // First, weed out any empty timezone or metazone names from myMap. |
| for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { |
| String key = it.next(); |
| if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX) |
| || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { |
| @SuppressWarnings("unchecked") |
| Map<String, String> nameMap = (Map<String, String>) myMap.get(key); |
| if (nameMap.isEmpty()) { |
| // Some zones have only exemplarCity, which become empty. |
| // Remove those from the map. |
| it.remove(); |
| continue; |
| } |
| } |
| } |
| for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { |
| String key = it.next(); |
| if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX) |
| || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) { |
| @SuppressWarnings("unchecked") |
| Map<String, String> nameMap = (Map<String, String>) myMap.get(key); |
| |
| // Convert key/value pairs to an array. |
| String[] names = new String[ZONE_NAME_KEYS.length]; |
| int ix = 0; |
| for (String nameKey : ZONE_NAME_KEYS) { |
| String name = nameMap.get(nameKey); |
| if (name == null && parentsMap != null) { |
| @SuppressWarnings("unchecked") |
| Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key); |
| if (parentNames != null) { |
| name = parentNames.get(nameKey); |
| } |
| } |
| names[ix++] = name; |
| } |
| if (hasNulls(names)) { |
| String metaKey = toMetaZoneKey(key); |
| if (metaKey != null) { |
| Object obj = myMap.get(metaKey); |
| if (obj instanceof String[]) { |
| String[] metaNames = (String[]) obj; |
| for (int i = 0; i < names.length; i++) { |
| if (names[i] == null) { |
| names[i] = metaNames[i]; |
| } |
| } |
| } else if (obj instanceof Map) { |
| @SuppressWarnings("unchecked") |
| Map<String, String> m = (Map<String, String>) obj; |
| for (int i = 0; i < names.length; i++) { |
| if (names[i] == null) { |
| names[i] = m.get(ZONE_NAME_KEYS[i]); |
| } |
| } |
| } |
| } |
| } |
| // replace the Map with the array |
| if (names != null) { |
| myMap.put(key, names); |
| } else { |
| it.remove(); |
| } |
| } |
| } |
| // replace empty era names with parentMap era names |
| for (String key : ERA_KEYS) { |
| Object value = myMap.get(key); |
| if (value != null && value instanceof String[]) { |
| String[] eraStrings = (String[]) value; |
| for (String eraString : eraStrings) { |
| if (eraString == null || eraString.isEmpty()) { |
| fillInElements(parentsMap, key, value); |
| } |
| } |
| } |
| } |
| |
| // rules |
| String rule = CLDRConverter.pluralRules.get(id); |
| if (rule != null) { |
| myMap.put("PluralRules", rule); |
| } |
| rule = CLDRConverter.dayPeriodRules.get(id); |
| if (rule != null) { |
| myMap.put("DayPeriodRules", rule); |
| } |
| |
| // Remove all duplicates |
| if (Objects.nonNull(parentsMap)) { |
| for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) { |
| String key = it.next(); |
| if (!key.equals("numberingScripts") && // real body "NumberElements" may differ |
| Objects.deepEquals(parentsMap.get(key), myMap.get(key))) { |
| it.remove(); |
| } |
| } |
| } |
| |
| targetMap = myMap; |
| return myMap; |
| } |
| |
| private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) { |
| String formatMapKey = key + "/format"; |
| Object format = map.get(formatMapKey); |
| if (format != null) { |
| map.remove(formatMapKey); |
| map.put(key, format); |
| if (fillInElements(parents, formatMapKey, format)) { |
| map.remove(key); |
| } |
| } |
| String standaloneMapKey = key + "/stand-alone"; |
| Object standalone = map.get(standaloneMapKey); |
| if (standalone != null) { |
| map.remove(standaloneMapKey); |
| String standaloneResourceKey = "standalone." + key; |
| map.put(standaloneResourceKey, standalone); |
| if (fillInElements(parents, standaloneMapKey, standalone)) { |
| map.remove(standaloneResourceKey); |
| } |
| } |
| } |
| |
| /** |
| * Fills in any empty elements with its parent element, falling back to |
| * aliased one if parent element is not found. Returns true if the resulting |
| * array is identical to its parent array. |
| * |
| * @param parents |
| * @param key |
| * @param value |
| * @return true if the resulting array is identical to its parent array. |
| */ |
| private boolean fillInElements(Map<String, Object> parents, String key, Object value) { |
| if (parents == null) { |
| return false; |
| } |
| if (value instanceof String[]) { |
| Object pvalue = parents.getOrDefault(key, parents.get(CLDRConverter.aliases.get(key))); |
| if (pvalue != null && pvalue instanceof String[]) { |
| String[] strings = (String[]) value; |
| String[] pstrings = (String[]) pvalue; |
| for (int i = 0; i < strings.length; i++) { |
| if (strings[i] == null || strings[i].length() == 0) { |
| strings[i] = pstrings[i]; |
| } |
| } |
| return Arrays.equals(strings, pstrings); |
| } |
| } |
| return false; |
| } |
| |
| /* |
| * Adjusts String[] for era names because JRE's Calendars use different |
| * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars. |
| */ |
| private void adjustEraNames(Map<String, Object> map, Map<String, Object> pMap, CalendarType type) { |
| String[][] eraNames = new String[ERA_KEYS.length][]; |
| String[] realKeys = new String[ERA_KEYS.length]; |
| int index = 0; |
| for (String key : ERA_KEYS) { |
| String realKey = type.keyElementName() + key; |
| String[] value = (String[]) map.get(realKey); |
| if (value != null) { |
| // first fill in missing elements from parents map. |
| fillInElements(pMap, realKey, value); |
| |
| switch (type) { |
| case GREGORIAN: |
| break; |
| |
| case JAPANESE: |
| { |
| String[] newValue = new String[value.length + 1]; |
| String[] julianEras = (String[]) map.get(key); |
| if (julianEras != null && julianEras.length >= 2) { |
| newValue[0] = julianEras[1]; |
| } else { |
| newValue[0] = ""; |
| } |
| System.arraycopy(value, 0, newValue, 1, value.length); |
| value = newValue; |
| |
| // fix up 'Reiwa' era, which can be missing in some locales |
| if (value[value.length - 1] == null) { |
| value[value.length - 1] = (key.startsWith("narrow.") ? "R" : "Reiwa"); |
| } |
| } |
| break; |
| |
| case BUDDHIST: |
| // Replace the value |
| value = new String[] {"BC", value[0]}; |
| break; |
| |
| case ISLAMIC: |
| // Replace the value |
| value = new String[] {"", value[0]}; |
| break; |
| } |
| map.put(realKey, value); |
| map.put("java.time." + realKey, value); |
| } |
| realKeys[index] = realKey; |
| eraNames[index++] = value; |
| } |
| for (int i = 0; i < eraNames.length; i++) { |
| if (eraNames[i] == null) { |
| map.put(realKeys[i], null); |
| } |
| } |
| } |
| |
| private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap, |
| CalendarType calendarType, String name) { |
| String calendarPrefix = calendarType.keyElementName(); |
| for (String k : patternKeys) { |
| if (myMap.containsKey(calendarPrefix + k)) { |
| int len = patternKeys.length; |
| List<String> dateTimePatterns = new ArrayList<>(len); |
| List<String> sdfPatterns = new ArrayList<>(len); |
| for (int i = 0; i < len; i++) { |
| String key = calendarPrefix + patternKeys[i]; |
| String pattern = (String) myMap.remove(key); |
| if (pattern == null) { |
| pattern = (String) parentsMap.remove(key); |
| } |
| if (pattern != null) { |
| // Perform date-time format pattern conversion which is |
| // applicable to both SimpleDateFormat and j.t.f.DateTimeFormatter. |
| String transPattern = translateDateFormatLetters(calendarType, key, pattern, this::convertDateTimePatternLetter); |
| dateTimePatterns.add(i, transPattern); |
| // Additionally, perform SDF specific date-time format pattern conversion |
| sdfPatterns.add(i, translateDateFormatLetters(calendarType, key, transPattern, this::convertSDFLetter)); |
| } else { |
| dateTimePatterns.add(i, null); |
| sdfPatterns.add(i, null); |
| } |
| } |
| // If empty, discard patterns |
| if (sdfPatterns.isEmpty()) { |
| return; |
| } |
| String key = calendarPrefix + name; |
| |
| // If additional changes are made in the SDF specific conversion, |
| // keep the commonly converted patterns as java.time patterns |
| if (!dateTimePatterns.equals(sdfPatterns)) { |
| myMap.put("java.time." + key, dateTimePatterns.toArray(String[]::new)); |
| } |
| myMap.put(key, sdfPatterns.toArray(new String[len])); |
| break; |
| } |
| } |
| } |
| |
| private String translateDateFormatLetters(CalendarType calendarType, String patternKey, String cldrFormat, ConvertDateTimeLetters converter) { |
| String pattern = cldrFormat; |
| int length = pattern.length(); |
| boolean inQuote = false; |
| StringBuilder jrePattern = new StringBuilder(length); |
| int count = 0; |
| char lastLetter = 0; |
| |
| for (int i = 0; i < length; i++) { |
| char c = pattern.charAt(i); |
| |
| if (c == '\'') { |
| // '' is treated as a single quote regardless of being |
| // in a quoted section. |
| if ((i + 1) < length) { |
| char nextc = pattern.charAt(i + 1); |
| if (nextc == '\'') { |
| i++; |
| if (count != 0) { |
| converter.convert(calendarType, patternKey, lastLetter, count, jrePattern); |
| lastLetter = 0; |
| count = 0; |
| } |
| jrePattern.append("''"); |
| continue; |
| } |
| } |
| if (!inQuote) { |
| if (count != 0) { |
| converter.convert(calendarType, patternKey, lastLetter, count, jrePattern); |
| lastLetter = 0; |
| count = 0; |
| } |
| inQuote = true; |
| } else { |
| inQuote = false; |
| } |
| jrePattern.append(c); |
| continue; |
| } |
| if (inQuote) { |
| jrePattern.append(c); |
| continue; |
| } |
| if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) { |
| if (count != 0) { |
| converter.convert(calendarType, patternKey, lastLetter, count, jrePattern); |
| lastLetter = 0; |
| count = 0; |
| } |
| jrePattern.append(c); |
| continue; |
| } |
| |
| if (lastLetter == 0 || lastLetter == c) { |
| lastLetter = c; |
| count++; |
| continue; |
| } |
| converter.convert(calendarType, patternKey, lastLetter, count, jrePattern); |
| lastLetter = c; |
| count = 1; |
| } |
| |
| if (inQuote) { |
| throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat); |
| } |
| |
| if (count != 0) { |
| converter.convert(calendarType, patternKey, lastLetter, count, jrePattern); |
| } |
| if (cldrFormat.contentEquals(jrePattern)) { |
| return cldrFormat; |
| } |
| return jrePattern.toString(); |
| } |
| |
| private String toMetaZoneKey(String tzKey) { |
| if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) { |
| String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length()); |
| String meta = CLDRConverter.handlerMetaZones.get(tz); |
| if (meta != null) { |
| return CLDRConverter.METAZONE_ID_PREFIX + meta; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Perform a generic conversion of CLDR date-time format pattern letter based |
| * on the support given by the SimpleDateFormat and the j.t.f.DateTimeFormatter |
| * for date-time formatting. |
| */ |
| private void convertDateTimePatternLetter(CalendarType calendarType, String patternKey, char cldrLetter, int count, StringBuilder sb) { |
| switch (cldrLetter) { |
| case 'u': |
| case 'U': |
| // Change cldr letter 'u'/'U' to 'y', as 'u' is interpreted as |
| // "Extended year (numeric)", and 'U' as "Cyclic year" in CLDR/LDML, |
| // which is not supported in SimpleDateFormat and |
| // j.t.f.DateTimeFormatter, so it is replaced with 'y' |
| // as the best approximation |
| appendN('y', count, sb); |
| break; |
| default: |
| appendN(cldrLetter, count, sb); |
| break; |
| |
| } |
| } |
| |
| /** |
| * Perform a conversion of CLDR date-time format pattern letter which is |
| * specific to the SimpleDateFormat. |
| */ |
| private void convertSDFLetter(CalendarType calendarType, String patternKey, char cldrLetter, int count, StringBuilder sb) { |
| switch (cldrLetter) { |
| case 'G': |
| if (calendarType != CalendarType.GREGORIAN) { |
| // Adjust the number of 'G's for JRE SimpleDateFormat |
| if (count == 5) { |
| // CLDR narrow -> JRE short |
| count = 1; |
| } else if (count == 1) { |
| // CLDR abbr -> JRE long |
| count = 4; |
| } |
| } |
| appendN(cldrLetter, count, sb); |
| break; |
| |
| // TODO: support 'c' and 'e' in JRE SimpleDateFormat |
| // Use 'u' and 'E' for now. |
| case 'c': |
| case 'e': |
| switch (count) { |
| case 1: |
| sb.append('u'); |
| break; |
| case 3: |
| case 4: |
| appendN('E', count, sb); |
| break; |
| case 5: |
| appendN('E', 3, sb); |
| break; |
| } |
| break; |
| |
| case 'v': |
| case 'V': |
| appendN('z', count, sb); |
| break; |
| |
| case 'y': |
| // If the style is FULL/LONG for a Japanese Calendar, make the |
| // count == 4 for Gan-nen |
| if (calendarType == CalendarType.JAPANESE && |
| (patternKey.contains("full-") || |
| patternKey.contains("long-"))) { |
| count = 4; |
| } |
| appendN(cldrLetter, count, sb); |
| break; |
| |
| case 'Z': |
| if (count == 4 || count == 5) { |
| sb.append("XXX"); |
| } |
| break; |
| |
| case 'B': |
| // 'B' character (day period) is not supported by SimpleDateFormat, |
| // this is a workaround in which 'B' character |
| // appearing in CLDR date-time pattern is replaced |
| // with 'a' character and hence resolved with am/pm strings. |
| // This workaround is based on the fallback mechanism |
| // specified in LDML spec for 'B' character, when a locale |
| // does not have data for day period ('B') |
| appendN('a', count, sb); |
| break; |
| |
| default: |
| appendN(cldrLetter, count, sb); |
| break; |
| } |
| } |
| |
| private void appendN(char c, int n, StringBuilder sb) { |
| for (int i = 0; i < n; i++) { |
| sb.append(c); |
| } |
| } |
| |
| private static boolean hasNulls(Object[] array) { |
| for (int i = 0; i < array.length; i++) { |
| if (array[i] == null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void handleSkeletonPatterns(Map<String, Object> myMap, CalendarType calendarType) { |
| String calendarPrefix = calendarType.keyElementName(); |
| myMap.putAll(myMap.entrySet().stream() |
| .filter(e -> e.getKey().startsWith(Bundle.DATEFORMATITEM_KEY_PREFIX)) |
| .collect(Collectors.toMap( |
| e -> calendarPrefix + e.getKey(), |
| e -> translateDateFormatLetters(calendarType, |
| e.getKey(), |
| (String)e.getValue(), |
| this::convertDateTimePatternLetter) |
| )) |
| ); |
| } |
| |
| @FunctionalInterface |
| private interface ConvertDateTimeLetters { |
| void convert(CalendarType calendarType, String patternKey, char cldrLetter, int count, StringBuilder sb); |
| } |
| |
| /** |
| * Returns a complete string array for NumberElements or NumberPatterns. If any |
| * array element is missing, it will fall back to parents map, as well as |
| * numbering script fallback. |
| */ |
| private String[] createNumberArray(Map<String, Object> myMap, Map<String, Object>parentsMap, |
| String[] keys, String script) { |
| String[] numArray = new String[keys.length]; |
| for (int i = 0; i < keys.length; i++) { |
| String key = script + "." + keys[i]; |
| final int idx = i; |
| Optional.ofNullable( |
| myMap.getOrDefault(key, |
| // if value not found in myMap, search for parentsMap |
| parentsMap.getOrDefault(key, |
| parentsMap.getOrDefault(keys[i], |
| // the last resort is "latn" |
| parentsMap.get("latn." + keys[i]))))) |
| .ifPresentOrElse(v -> numArray[idx] = (String)v, () -> { |
| if (keys == NUMBER_PATTERN_KEYS) { |
| // NumberPatterns |
| if (!key.endsWith("accounting")) { |
| // throw error unless it is for "accounting", |
| // which may be missing. |
| throw new InternalError("NumberPatterns: null for " + |
| key + ", id: " + id); |
| } |
| } else { |
| // NumberElements |
| assert keys == NUMBER_ELEMENT_KEYS; |
| if (key.endsWith("/pattern")) { |
| numArray[idx] = "#"; |
| } else if (!key.endsWith("currencyDecimal") && |
| !key.endsWith("currencyGroup")) { |
| // throw error unless it is for "currencyDecimal/Group", |
| // which may be missing. |
| throw new InternalError("NumberElements: null for " + |
| key + ", id: " + id); |
| } |
| }}); |
| } |
| return numArray; |
| } |
| |
| private static void skeletonInputRegions(Map<String, Object> myMap) { |
| myMap.putAll(myMap.entrySet().stream() |
| .filter(e -> e.getKey().startsWith(Bundle.DATEFORMATITEM_INPUT_REGIONS_PREFIX)) |
| .collect(Collectors.toMap( |
| e -> e.getKey(), |
| e -> ((String)e.getValue()).trim() |
| )) |
| ); |
| } |
| } |