Home | History | Annotate | Download | only in impl
      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 
     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 
     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 
     76         private DayPeriodRulesDataSink(DayPeriodRulesData data) {
     77             this.data = data;
     78         }
     79 
     80         @Override
     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 
     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.
    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 
    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 
    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 
    210         private DayPeriodRulesCountSink(DayPeriodRulesData data) {
    211             this.data = data;
    212         }
    213 
    214         @Override
    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 
    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      */
    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 
    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 
    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 
    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 
    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.
    354     public boolean hasMidnight() { return hasMidnight; }
    355     public boolean hasNoon() { return hasNoon; }
    356     public DayPeriod getDayPeriodForHour(int hour) { return dayPeriodForHour[hour]; }
    357 
    358     // Helpers.
    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 
    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