1 package org.unicode.cldr.util; 2 3 import java.util.HashSet; 4 import java.util.List; 5 import java.util.Set; 6 import java.util.TreeSet; 7 8 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod; 9 import org.unicode.cldr.util.PluralRulesUtil.KeywordStatus; 10 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo; 11 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count; 12 import org.unicode.cldr.util.SupplementalDataInfo.PluralType; 13 14 import com.google.common.collect.ImmutableList; 15 import com.google.common.collect.ImmutableSet; 16 import com.ibm.icu.text.PluralRules; 17 18 public class LogicalGrouping { 19 20 public static final ImmutableSet<String> metazonesDSTSet = ImmutableSet.of( 21 "Acre", "Africa_Western", "Alaska", "Almaty", "Amazon", 22 "America_Central", "America_Eastern", "America_Mountain", "America_Pacific", "Anadyr", "Apia", 23 "Aqtau", "Aqtobe", "Arabian", "Argentina", "Argentina_Western", "Armenia", 24 "Atlantic", "Australia_Central", "Australia_CentralWestern", "Australia_Eastern", "Australia_Western", 25 "Azerbaijan", "Azores", "Bangladesh", "Brasilia", "Cape_Verde", 26 "Chatham", "Chile", "China", "Choibalsan", "Colombia", "Cook", "Cuba", "Easter", 27 "Europe_Central", "Europe_Eastern", "Europe_Western", "Falkland", "Fiji", "Georgia", 28 "Greenland_Eastern", "Greenland_Western", "Hawaii_Aleutian", "Hong_Kong", "Hovd", 29 "Iran", "Irkutsk", "Israel", "Japan", "Kamchatka", "Korea", "Krasnoyarsk", 30 "Lord_Howe", "Macau", "Magadan", "Mauritius", "Mexico_Northwest", "Mexico_Pacific", "Mongolia", "Moscow", "New_Caledonia", 31 "New_Zealand", "Newfoundland", "Noronha", "Novosibirsk", "Omsk", "Pakistan", "Paraguay", "Peru", "Philippines", 32 "Pierre_Miquelon", "Qyzylorda", "Sakhalin", "Samara", "Samoa", 33 "Taipei", "Tonga", "Turkmenistan", "Uruguay", "Uzbekistan", 34 "Vanuatu", "Vladivostok", "Volgograd", "Yakutsk", "Yekaterinburg"); 35 36 public static final ImmutableList<String> days = ImmutableList.of("sun", "mon", "tue", "wed", "thu", "fri", "sat"); 37 38 public static final ImmutableSet<String> calendarsWith13Months = ImmutableSet.of("coptic", "ethiopic", "hebrew"); 39 public static final ImmutableSet<String> compactDecimalFormatLengths = ImmutableSet.of("short", "long"); 40 private static final ImmutableSet<String> ampm = ImmutableSet.of("am", "pm"); 41 private static final ImmutableSet<String> nowUnits = ImmutableSet.of("second", "second-short", "second-narrow", 42 "minute", "minute-short", "minute-narrow", "hour", "hour-short", "hour-narrow"); 43 44 /** 45 * Return the set of paths that are in the same logical set as the given path 46 * 47 * @param path 48 * - the distinguishing xpath 49 */ getPaths(CLDRFile cldrFile, String path)50 public static Set<String> getPaths(CLDRFile cldrFile, String path) { 51 ImmutableSet<String> metazone_string_types = ImmutableSet.of("generic", "standard", "daylight"); 52 53 Set<String> result = new TreeSet<String>(); 54 if (path == null) return result; 55 result.add(path); 56 // Figure out the plurals forms, as we will probably need them. 57 58 XPathParts parts = new XPathParts(); 59 parts.set(path); 60 61 if (path.indexOf("/metazone") > 0) { 62 String metazoneName = parts.getAttributeValue(3, "type"); 63 if (metazonesDSTSet.contains(metazoneName)) { 64 for (String str : metazone_string_types) { 65 result.add(path.substring(0, path.lastIndexOf('/') + 1) + str); 66 } 67 } 68 } else if (path.indexOf("/days") > 0) { 69 String dayName = parts.size() > 7 ? parts.getAttributeValue(7, "type") : null; 70 if (dayName != null && days.contains(dayName)) { // This is just a quick check to make sure the path is 71 // good. 72 for (String str : days) { 73 parts.setAttribute("day", "type", str); 74 result.add(parts.toString()); 75 } 76 } 77 } else if (path.indexOf("/dayPeriods") > 0) { 78 if (path.endsWith("alias")) { 79 result.add(path); 80 } else { 81 String dayPeriodType = parts.findAttributeValue("dayPeriod", "type"); 82 83 if (ampm.contains(dayPeriodType)) { 84 for (String s : ampm) { 85 parts.setAttribute("dayPeriod", "type", s); 86 result.add(parts.toString()); 87 } 88 } else { 89 SupplementalDataInfo supplementalData = SupplementalDataInfo.getInstance( 90 cldrFile.getSupplementalDirectory()); 91 DayPeriodInfo.Type dayPeriodContext = DayPeriodInfo.Type.fromString(parts.findAttributeValue("dayPeriodContext", "type")); 92 DayPeriodInfo dpi = supplementalData.getDayPeriods(dayPeriodContext, cldrFile.getLocaleID()); 93 List<DayPeriod> dayPeriods = dpi.getPeriods(); 94 DayPeriod thisDayPeriod = DayPeriod.fromString(dayPeriodType); 95 if (dayPeriods.contains(thisDayPeriod)) { 96 for (DayPeriod d : dayPeriods) { 97 parts.setAttribute("dayPeriod", "type", d.name()); 98 result.add(parts.toString()); 99 } 100 } 101 } 102 } 103 } else if (path.indexOf("/quarters") > 0) { 104 String quarterName = parts.size() > 7 ? parts.getAttributeValue(7, "type") : null; 105 Integer quarter = quarterName == null ? 0 : Integer.valueOf(quarterName); 106 if (quarter > 0 && quarter <= 4) { // This is just a quick check to make sure the path is good. 107 for (Integer i = 1; i <= 4; i++) { 108 parts.setAttribute("quarter", "type", i.toString()); 109 result.add(parts.toString()); 110 } 111 } 112 } else if (path.indexOf("/months") > 0) { 113 String calType = parts.size() > 3 ? parts.getAttributeValue(3, "type") : null; 114 String monthName = parts.size() > 7 ? parts.getAttributeValue(7, "type") : null; 115 Integer month = monthName == null ? 0 : Integer.valueOf(monthName); 116 int calendarMonthMax = calendarsWith13Months.contains(calType) ? 13 : 12; 117 if (month > 0 && month <= calendarMonthMax) { // This is just a quick check to make sure the path is good. 118 for (Integer i = 1; i <= calendarMonthMax; i++) { 119 parts.setAttribute("month", "type", i.toString()); 120 if ("hebrew".equals(calType)) { 121 parts.removeAttribute("month", "yeartype"); 122 } 123 result.add(parts.toString()); 124 } 125 if ("hebrew".equals(calType)) { // Add extra hebrew calendar leap month 126 parts.setAttribute("month", "type", Integer.toString(7)); 127 parts.setAttribute("month", "yeartype", "leap"); 128 result.add(parts.toString()); 129 } 130 } 131 } else if (parts.containsElement("relative")) { 132 String fieldType = parts.findAttributeValue("field", "type"); 133 String relativeType = parts.findAttributeValue("relative", "type"); 134 Integer relativeValue = relativeType == null ? 999 : Integer.valueOf(relativeType); 135 if (relativeValue >= -3 && relativeValue <= 3) { // This is just a quick check to make sure the path is good. 136 if (!(nowUnits.contains(fieldType) && relativeValue == 0)) { // Workaround for "now", "this hour", "this minute" 137 int limit = 1; 138 if (fieldType != null && fieldType.startsWith("day")) { 139 limit = 3; 140 } 141 for (Integer i = -1 * limit; i <= limit; i++) { 142 parts.setAttribute("relative", "type", i.toString()); 143 result.add(parts.toString()); 144 } 145 } 146 } 147 } else if (path.indexOf("/decimalFormatLength") > 0) { 148 PluralInfo pluralInfo = getPluralInfo(cldrFile); 149 Set<Count> pluralTypes = pluralInfo.getCounts(); 150 String decimalFormatLengthType = parts.size() > 3 ? parts.getAttributeValue(3, "type") : null; 151 String decimalFormatPatternType = parts.size() > 5 ? parts.getAttributeValue(5, "type") : null; 152 if (decimalFormatLengthType != null && decimalFormatPatternType != null && 153 compactDecimalFormatLengths.contains(decimalFormatLengthType)) { 154 int numZeroes = decimalFormatPatternType.length() - 1; 155 int baseZeroes = (numZeroes / 3) * 3; 156 for (int i = 0; i < 3; i++) { 157 String patType = "1" + String.format(String.format("%%0%dd", baseZeroes + i), 0); // This gives us "baseZeroes+i" zeroes at the end. 158 parts.setAttribute(5, "type", patType); 159 for (Count count : pluralTypes) { 160 parts.setAttribute(5, "count", count.toString()); 161 result.add(parts.toString()); 162 } 163 } 164 } 165 } else if (path.indexOf("[@count=") > 0) { 166 PluralInfo pluralInfo = getPluralInfo(cldrFile); 167 Set<Count> pluralTypes = pluralInfo.getCounts(); 168 String lastElement = parts.getElement(-1); 169 for (Count count : pluralTypes) { 170 parts.setAttribute(lastElement, "count", count.toString()); 171 result.add(parts.toString()); 172 } 173 } 174 return result; 175 } 176 177 /** 178 * Returns the plural info for a given locale. 179 */ getPluralInfo(CLDRFile cldrFile)180 private static PluralInfo getPluralInfo(CLDRFile cldrFile) { 181 SupplementalDataInfo supplementalData = SupplementalDataInfo.getInstance( 182 cldrFile.getSupplementalDirectory()); 183 return supplementalData.getPlurals(PluralType.cardinal, 184 cldrFile.getLocaleID()); 185 } 186 187 /** 188 * @param cldrFile 189 * @param path 190 * @return true if the specified path is optional in the logical grouping 191 * that it belongs to. 192 */ isOptional(CLDRFile cldrFile, String path)193 public static boolean isOptional(CLDRFile cldrFile, String path) { 194 XPathParts parts = new XPathParts().set(path); 195 196 if (parts.containsElement("relative")) { 197 String fieldType = parts.findAttributeValue("field", "type"); 198 String relativeType = parts.findAttributeValue("relative", "type"); 199 Integer relativeValue = relativeType == null ? 999 : Integer.valueOf(relativeType); 200 if (fieldType != null && fieldType.startsWith("day") && Math.abs(relativeValue.intValue()) >= 2) { 201 return true; // relative days +2 +3 -2 -3 are optional in a logical group. 202 } 203 } 204 // Paths with count="(zero|one)" are optional if their usage is covered 205 // fully by paths with count="(0|1)", which are always optional themselves. 206 if (!path.contains("[@count=")) return false; 207 String pluralType = parts.getAttributeValue(-1, "count"); 208 if (pluralType.equals("0") || pluralType.equals("1")) return true; 209 if (!pluralType.equals("zero") && !pluralType.equals("one")) return false; 210 211 PluralRules pluralRules = getPluralInfo(cldrFile).getPluralRules(); 212 parts.setAttribute(-1, "count", "0"); 213 Set<Double> explicits = new HashSet<Double>(); 214 if (cldrFile.isHere(parts.toString())) { 215 explicits.add(0.0); 216 } 217 parts.setAttribute(-1, "count", "1"); 218 if (cldrFile.isHere(parts.toString())) { 219 explicits.add(1.0); 220 } 221 if (!explicits.isEmpty()) { 222 // HACK: The com.ibm.icu.text prefix is needed so that ST can find it 223 // (no idea why). 224 KeywordStatus status = org.unicode.cldr.util.PluralRulesUtil.getKeywordStatus( 225 pluralRules, pluralType, 0, explicits, true); 226 if (status == KeywordStatus.SUPPRESSED) { 227 return true; 228 } 229 } 230 return false; 231 } 232 } 233