1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html#License 3 /* 4 ******************************************************************************* 5 * Copyright (C) 2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 ******************************************************************************* 8 */ 9 package com.ibm.icu.impl; 10 11 import java.util.HashMap; 12 import java.util.Map; 13 14 import com.ibm.icu.util.ICUException; 15 import com.ibm.icu.util.ULocale; 16 import com.ibm.icu.util.UResourceBundle; 17 18 public final class DayPeriodRules { 19 public enum DayPeriod { 20 MIDNIGHT, 21 NOON, 22 MORNING1, 23 AFTERNOON1, 24 EVENING1, 25 NIGHT1, 26 MORNING2, 27 AFTERNOON2, 28 EVENING2, 29 NIGHT2, 30 AM, 31 PM; 32 33 public static DayPeriod[] VALUES = DayPeriod.values(); 34 fromStringOrNull(CharSequence str)35 private static DayPeriod fromStringOrNull(CharSequence str) { 36 if ("midnight".contentEquals(str)) { return MIDNIGHT; } 37 if ("noon".contentEquals(str)) { return NOON; } 38 if ("morning1".contentEquals(str)) { return MORNING1; } 39 if ("afternoon1".contentEquals(str)) { return AFTERNOON1; } 40 if ("evening1".contentEquals(str)) { return EVENING1; } 41 if ("night1".contentEquals(str)) { return NIGHT1; } 42 if ("morning2".contentEquals(str)) { return MORNING2; } 43 if ("afternoon2".contentEquals(str)) { return AFTERNOON2; } 44 if ("evening2".contentEquals(str)) { return EVENING2; } 45 if ("night2".contentEquals(str)) { return NIGHT2; } 46 if ("am".contentEquals(str)) { return AM; } 47 if ("pm".contentEquals(str)) { return PM; } 48 return null; 49 } 50 } 51 52 private enum CutoffType { 53 BEFORE, 54 AFTER, // TODO: AFTER is deprecated in CLDR 29. Remove. 55 FROM, 56 AT; 57 fromStringOrNull(CharSequence str)58 private static CutoffType fromStringOrNull(CharSequence str) { 59 if ("from".contentEquals(str)) { return CutoffType.FROM; } 60 if ("before".contentEquals(str)) { return CutoffType.BEFORE; } 61 if ("after".contentEquals(str)) { return CutoffType.AFTER; } 62 if ("at".contentEquals(str)) { return CutoffType.AT; } 63 return null; 64 } 65 } 66 67 private static final class DayPeriodRulesData { 68 Map<String, Integer> localesToRuleSetNumMap = new HashMap<String, Integer>(); 69 DayPeriodRules[] rules; 70 int maxRuleSetNum = -1; 71 } 72 73 private static final class DayPeriodRulesDataSink extends UResource.Sink { 74 private DayPeriodRulesData data; 75 DayPeriodRulesDataSink(DayPeriodRulesData data)76 private DayPeriodRulesDataSink(DayPeriodRulesData data) { 77 this.data = data; 78 } 79 80 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)81 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 82 UResource.Table dayPeriodData = value.getTable(); 83 for (int i = 0; dayPeriodData.getKeyAndValue(i, key, value); ++i) { 84 if (key.contentEquals("locales")) { 85 UResource.Table locales = value.getTable(); 86 for (int j = 0; locales.getKeyAndValue(j, key, value); ++j) { 87 int setNum = parseSetNum(value.getString()); 88 data.localesToRuleSetNumMap.put(key.toString(), setNum); 89 } 90 } else if (key.contentEquals("rules")) { 91 UResource.Table rules = value.getTable(); 92 processRules(rules, key, value); 93 } 94 } 95 } 96 processRules(UResource.Table rules, UResource.Key key, UResource.Value value)97 private void processRules(UResource.Table rules, UResource.Key key, UResource.Value value) { 98 for (int i = 0; rules.getKeyAndValue(i, key, value); ++i) { 99 ruleSetNum = parseSetNum(key.toString()); 100 data.rules[ruleSetNum] = new DayPeriodRules(); 101 102 UResource.Table ruleSet = value.getTable(); 103 for (int j = 0; ruleSet.getKeyAndValue(j, key, value); ++j) { 104 period = DayPeriod.fromStringOrNull(key); 105 if (period == null) { throw new ICUException("Unknown day period in data."); } 106 107 UResource.Table periodDefinition = value.getTable(); 108 for (int k = 0; periodDefinition.getKeyAndValue(k, key, value); ++k) { 109 if (value.getType() == UResourceBundle.STRING) { 110 // Key-value pairs (e.g. before{6:00}) 111 CutoffType type = CutoffType.fromStringOrNull(key); 112 addCutoff(type, value.getString()); 113 } else { 114 // Arrays (e.g. before{6:00, 24:00} 115 cutoffType = CutoffType.fromStringOrNull(key); 116 UResource.Array cutoffArray = value.getArray(); 117 int length = cutoffArray.getSize(); 118 for (int l = 0; l < length; ++l) { 119 cutoffArray.getValue(l, value); 120 addCutoff(cutoffType, value.getString()); 121 } 122 } 123 } 124 setDayPeriodForHoursFromCutoffs(); 125 for (int k = 0; k < cutoffs.length; ++k) { 126 cutoffs[k] = 0; 127 } 128 } 129 for (DayPeriod period : data.rules[ruleSetNum].dayPeriodForHour) { 130 if (period == null) { 131 throw new ICUException("Rules in data don't cover all 24 hours (they should)."); 132 } 133 } 134 } 135 } 136 137 // Members. 138 private int cutoffs[] = new int[25]; // [0] thru [24]; 24 is allowed is "before 24". 139 140 // "Path" to data. 141 private int ruleSetNum; 142 private DayPeriod period; 143 private CutoffType cutoffType; 144 145 // Helpers. addCutoff(CutoffType type, String hourStr)146 private void addCutoff(CutoffType type, String hourStr) { 147 if (type == null) { throw new ICUException("Cutoff type not recognized."); } 148 int hour = parseHour(hourStr); 149 cutoffs[hour] |= 1 << type.ordinal(); 150 } 151 setDayPeriodForHoursFromCutoffs()152 private void setDayPeriodForHoursFromCutoffs() { 153 DayPeriodRules rule = data.rules[ruleSetNum]; 154 for (int startHour = 0; startHour <= 24; ++startHour) { 155 // AT cutoffs must be either midnight or noon. 156 if ((cutoffs[startHour] & (1 << CutoffType.AT.ordinal())) > 0) { 157 if (startHour == 0 && period == DayPeriod.MIDNIGHT) { 158 rule.hasMidnight = true; 159 } else if (startHour == 12 && period == DayPeriod.NOON) { 160 rule.hasNoon = true; 161 } else { 162 throw new ICUException("AT cutoff must only be set for 0:00 or 12:00."); 163 } 164 } 165 166 // FROM/AFTER and BEFORE must come in a pair. 167 if ((cutoffs[startHour] & (1 << CutoffType.FROM.ordinal())) > 0 || 168 (cutoffs[startHour] & (1 << CutoffType.AFTER.ordinal())) > 0) { 169 for (int hour = startHour + 1;; ++hour) { 170 if (hour == startHour) { 171 // We've gone around the array once and can't find a BEFORE. 172 throw new ICUException( 173 "FROM/AFTER cutoffs must have a matching BEFORE cutoff."); 174 } 175 if (hour == 25) { hour = 0; } 176 if ((cutoffs[hour] & (1 << CutoffType.BEFORE.ordinal())) > 0) { 177 rule.add(startHour, hour, period); 178 break; 179 } 180 } 181 } 182 } 183 } 184 parseHour(String str)185 private static int parseHour(String str) { 186 int firstColonPos = str.indexOf(':'); 187 if (firstColonPos < 0 || !str.substring(firstColonPos).equals(":00")) { 188 throw new ICUException("Cutoff time must end in \":00\"."); 189 } 190 191 String hourStr = str.substring(0, firstColonPos); 192 if (firstColonPos != 1 && firstColonPos != 2) { 193 throw new ICUException("Cutoff time must begin with h: or hh:"); 194 } 195 196 int hour = Integer.parseInt(hourStr); 197 // parseInt() throws NumberFormatException if hourStr isn't proper. 198 199 if (hour < 0 || hour > 24) { 200 throw new ICUException("Cutoff hour must be between 0 and 24, inclusive."); 201 } 202 203 return hour; 204 } 205 } // DayPeriodRulesDataSink 206 207 private static class DayPeriodRulesCountSink extends UResource.Sink { 208 private DayPeriodRulesData data; 209 DayPeriodRulesCountSink(DayPeriodRulesData data)210 private DayPeriodRulesCountSink(DayPeriodRulesData data) { 211 this.data = data; 212 } 213 214 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)215 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 216 UResource.Table rules = value.getTable(); 217 for (int i = 0; rules.getKeyAndValue(i, key, value); ++i) { 218 int setNum = parseSetNum(key.toString()); 219 if (setNum > data.maxRuleSetNum) { 220 data.maxRuleSetNum = setNum; 221 } 222 } 223 } 224 } 225 226 private static final DayPeriodRulesData DATA = loadData(); 227 228 private boolean hasMidnight; 229 private boolean hasNoon; 230 private DayPeriod[] dayPeriodForHour; 231 DayPeriodRules()232 private DayPeriodRules() { 233 hasMidnight = false; 234 hasNoon = false; 235 dayPeriodForHour = new DayPeriod[24]; 236 } 237 238 /** 239 * Get a DayPeriodRules object given a locale. 240 * If data hasn't been loaded, it will be loaded for all locales at once. 241 * @param locale locale for which the DayPeriodRules object is requested. 242 * @return a DayPeriodRules object for `locale`. 243 */ getInstance(ULocale locale)244 public static DayPeriodRules getInstance(ULocale locale) { 245 String localeCode = locale.getBaseName(); 246 if (localeCode.isEmpty()) { localeCode = "root"; } 247 248 Integer ruleSetNum = null; 249 while (ruleSetNum == null) { 250 ruleSetNum = DATA.localesToRuleSetNumMap.get(localeCode); 251 if (ruleSetNum == null) { 252 localeCode = ULocale.getFallback(localeCode); 253 if (localeCode.isEmpty()) { 254 // Saves a lookup in the map. 255 break; 256 } 257 } else { 258 break; 259 } 260 } 261 262 if (ruleSetNum == null || DATA.rules[ruleSetNum] == null) { 263 // Data doesn't exist for the locale requested. 264 return null; 265 } 266 267 return DATA.rules[ruleSetNum]; 268 } 269 getMidPointForDayPeriod(DayPeriod dayPeriod)270 public double getMidPointForDayPeriod(DayPeriod dayPeriod) { 271 int startHour = getStartHourForDayPeriod(dayPeriod); 272 int endHour = getEndHourForDayPeriod(dayPeriod); 273 274 double midPoint = (startHour + endHour) / 2.0; 275 276 if (startHour > endHour) { 277 // dayPeriod wraps around midnight. Shift midPoint by 12 hours, in the direction that 278 // lands it in [0, 24). 279 midPoint += 12; 280 if (midPoint >= 24) { 281 midPoint -= 24; 282 } 283 } 284 285 return midPoint; 286 } 287 loadData()288 private static DayPeriodRulesData loadData() { 289 DayPeriodRulesData data = new DayPeriodRulesData(); 290 ICUResourceBundle rb = ICUResourceBundle.getBundleInstance( 291 ICUData.ICU_BASE_NAME, 292 "dayPeriods", 293 ICUResourceBundle.ICU_DATA_CLASS_LOADER, 294 true); 295 296 DayPeriodRulesCountSink countSink = new DayPeriodRulesCountSink(data); 297 rb.getAllItemsWithFallback("rules", countSink); 298 299 data.rules = new DayPeriodRules[data.maxRuleSetNum + 1]; 300 DayPeriodRulesDataSink sink = new DayPeriodRulesDataSink(data); 301 rb.getAllItemsWithFallback("", sink); 302 303 return data; 304 } 305 getStartHourForDayPeriod(DayPeriod dayPeriod)306 private int getStartHourForDayPeriod(DayPeriod dayPeriod) throws IllegalArgumentException { 307 if (dayPeriod == DayPeriod.MIDNIGHT) { return 0; } 308 if (dayPeriod == DayPeriod.NOON) { return 12; } 309 310 if (dayPeriodForHour[0] == dayPeriod && dayPeriodForHour[23] == dayPeriod) { 311 // dayPeriod wraps around midnight. Start hour is later than end hour. 312 for (int i = 22; i >= 1; --i) { 313 if (dayPeriodForHour[i] != dayPeriod) { 314 return (i + 1); 315 } 316 } 317 } else { 318 for (int i = 0; i <= 23; ++i) { 319 if (dayPeriodForHour[i] == dayPeriod) { 320 return i; 321 } 322 } 323 } 324 325 // dayPeriod doesn't exist in rule set; throw exception. 326 throw new IllegalArgumentException(); 327 } 328 getEndHourForDayPeriod(DayPeriod dayPeriod)329 private int getEndHourForDayPeriod(DayPeriod dayPeriod) { 330 if (dayPeriod == DayPeriod.MIDNIGHT) { return 0; } 331 if (dayPeriod == DayPeriod.NOON) { return 12; } 332 333 if (dayPeriodForHour[0] == dayPeriod && dayPeriodForHour[23] == dayPeriod) { 334 // dayPeriod wraps around midnight. End hour is before start hour. 335 for (int i = 1; i <= 22; ++i) { 336 if (dayPeriodForHour[i] != dayPeriod) { 337 // i o'clock is when a new period starts, therefore when the old period ends. 338 return i; 339 } 340 } 341 } else { 342 for (int i = 23; i >= 0; --i) { 343 if (dayPeriodForHour[i] == dayPeriod) { 344 return (i + 1); 345 } 346 } 347 } 348 349 // dayPeriod doesn't exist in rule set; throw exception. 350 throw new IllegalArgumentException(); 351 } 352 353 // Getters. hasMidnight()354 public boolean hasMidnight() { return hasMidnight; } hasNoon()355 public boolean hasNoon() { return hasNoon; } getDayPeriodForHour(int hour)356 public DayPeriod getDayPeriodForHour(int hour) { return dayPeriodForHour[hour]; } 357 358 // Helpers. add(int startHour, int limitHour, DayPeriod period)359 private void add(int startHour, int limitHour, DayPeriod period) { 360 for (int i = startHour; i != limitHour; ++i) { 361 if (i == 24) { i = 0; } 362 dayPeriodForHour[i] = period; 363 } 364 } 365 parseSetNum(String setNumStr)366 private static int parseSetNum(String setNumStr) { 367 if (!setNumStr.startsWith("set")) { 368 throw new ICUException("Set number should start with \"set\"."); 369 } 370 371 String numStr = setNumStr.substring(3); // e.g. "set17" -> "17" 372 return Integer.parseInt(numStr); // This throws NumberFormatException if numStr isn't a proper number. 373 } 374 } 375