Home | History | Annotate | Download | only in util
      1 package org.unicode.cldr.util;
      2 
      3 import java.util.ArrayList;
      4 import java.util.Arrays;
      5 import java.util.Collections;
      6 import java.util.EnumMap;
      7 import java.util.EnumSet;
      8 import java.util.LinkedHashSet;
      9 import java.util.List;
     10 import java.util.Map.Entry;
     11 import java.util.Set;
     12 import java.util.TreeSet;
     13 
     14 import com.ibm.icu.impl.Relation;
     15 import com.ibm.icu.impl.Row;
     16 import com.ibm.icu.impl.Row.R3;
     17 import com.ibm.icu.util.Output;
     18 
     19 public class DayPeriodInfo {
     20     public static final int HOUR = 60 * 60 * 1000;
     21     public static final int MIDNIGHT = 0;
     22     public static final int NOON = 12 * HOUR;
     23     public static final int DAY_LIMIT = 24 * HOUR;
     24 
     25     public enum Type {
     26         format("format"), selection("stand-alone");
     27         public final String pathValue;
     28 
     29         private Type(String _pathValue) {
     30             pathValue = _pathValue;
     31         }
     32 
     33         public static Type fromString(String source) {
     34             return selection.pathValue.equals(source) ? selection : Type.valueOf(source);
     35         }
     36     }
     37 
     38     public static class Span implements Comparable<Span> {
     39         public final int start;
     40         public final int end;
     41         public final boolean includesEnd;
     42         public final DayPeriod dayPeriod;
     43 
     44         public Span(int start, int end, DayPeriod dayPeriod) {
     45             this.start = start;
     46             this.end = end;
     47             this.includesEnd = start == end;
     48             this.dayPeriod = dayPeriod;
     49         }
     50 
     51         @Override
     52         public int compareTo(Span o) {
     53             int diff = start - o.start;
     54             if (diff != 0) {
     55                 return diff;
     56             }
     57             diff = end - o.end;
     58             if (diff != 0) {
     59                 return diff;
     60             }
     61             // because includesEnd is determined by the above, we're done
     62             return 0;
     63         }
     64 
     65         public boolean contains(int millisInDay) {
     66             return start <= millisInDay && (millisInDay < end || millisInDay == end && includesEnd);
     67         }
     68 
     69         /**
     70          * Returns end, but if not includesEnd, adjusted down by one.
     71          * @return
     72          */
     73         public int getAdjustedEnd() {
     74             return includesEnd ? end : end - 1;
     75         }
     76 
     77         @Override
     78         public boolean equals(Object obj) {
     79             Span other = (Span) obj;
     80             return start == other.start && end == other.end;
     81             // because includesEnd is determined by the above, we're done
     82         }
     83 
     84         @Override
     85         public int hashCode() {
     86             return start * 37 + end;
     87         }
     88 
     89         @Override
     90         public String toString() {
     91             return dayPeriod + ":" + toStringPlain();
     92         }
     93 
     94         public String toStringPlain() {
     95             return formatTime(start) + "  " + formatTime(end) + (includesEnd ? "" : "");
     96         }
     97     }
     98 
     99     public enum DayPeriod {
    100         // fixed
    101         midnight(MIDNIGHT, MIDNIGHT), am(MIDNIGHT, NOON), noon(NOON, NOON), pm(NOON, DAY_LIMIT),
    102         // flexible
    103         morning1, morning2, afternoon1, afternoon2, evening1, evening2, night1, night2;
    104 
    105         public final Span span;
    106 
    107         private DayPeriod(int start, int end) {
    108             span = new Span(start, end, this);
    109         }
    110 
    111         private DayPeriod() {
    112             span = null;
    113         }
    114 
    115         public static DayPeriod fromString(String value) {
    116             return valueOf(value);
    117         }
    118 
    119         public boolean isFixed() {
    120             return span != null;
    121         }
    122     };
    123 
    124     // the arrays must be in sorted order. First must have start= zero. Last must have end = DAY_LIMIT (and !includesEnd)
    125     // each of these will have the same length, and correspond.
    126     final private Span[] spans;
    127     final private DayPeriodInfo.DayPeriod[] dayPeriods;
    128     final Relation<DayPeriod, Span> dayPeriodsToSpans = Relation.of(new EnumMap<DayPeriod, Set<Span>>(DayPeriod.class), LinkedHashSet.class);
    129 
    130     public static class Builder {
    131         TreeSet<Span> info = new TreeSet<>();
    132 
    133         // TODO add rule test that they can't span same 12 hour time.
    134 
    135         public DayPeriodInfo.Builder add(DayPeriodInfo.DayPeriod dayPeriod, int start, boolean includesStart, int end,
    136             boolean includesEnd) {
    137             if (dayPeriod == null || start < 0 || start > end || end > DAY_LIMIT
    138                 || end - start > NOON) { // the span can't exceed 12 hours
    139                 throw new IllegalArgumentException("Bad data");
    140             }
    141             Span span = new Span(start, end, dayPeriod);
    142             boolean didntContain = info.add(span);
    143             if (!didntContain) {
    144                 throw new IllegalArgumentException("Duplicate data: " + span);
    145             }
    146             return this;
    147         }
    148 
    149         public DayPeriodInfo finish(String[] locales) {
    150             DayPeriodInfo result = new DayPeriodInfo(info, locales);
    151             info.clear();
    152             return result;
    153         }
    154 
    155         public boolean contains(DayPeriod dayPeriod) {
    156             for (Span span : info) {
    157                 if (span.dayPeriod == dayPeriod) {
    158                     return true;
    159                 }
    160             }
    161             return false;
    162         }
    163     }
    164 
    165     private DayPeriodInfo(TreeSet<Span> info, String[] locales) {
    166         int len = info.size();
    167         spans = info.toArray(new Span[len]);
    168         List<DayPeriod> tempPeriods = new ArrayList<>();
    169         // check data
    170         if (len != 0) {
    171             Span last = spans[0];
    172             tempPeriods.add(last.dayPeriod);
    173             dayPeriodsToSpans.put(last.dayPeriod, last);
    174             if (last.start != MIDNIGHT) {
    175                 throw new IllegalArgumentException("Doesn't start at 0:00).");
    176             }
    177             for (int i = 1; i < len; ++i) {
    178                 Span current = spans[i];
    179                 if (current.start != current.end && last.start != last.end) {
    180                     if (current.start != last.end) {
    181                         throw new IllegalArgumentException("Gap or overlapping times:\t"
    182                             + current + "\t" + last + "\t" + Arrays.asList(locales));
    183                     }
    184                 }
    185                 tempPeriods.add(current.dayPeriod);
    186                 dayPeriodsToSpans.put(current.dayPeriod, current);
    187                 last = current;
    188             }
    189             if (last.end != DAY_LIMIT) {
    190                 throw new IllegalArgumentException("Doesn't reach 24:00).");
    191             }
    192         }
    193         dayPeriods = tempPeriods.toArray(new DayPeriod[len]);
    194         dayPeriodsToSpans.freeze();
    195         // add an extra check to make sure that periods are unique over 12 hour spans
    196         for (Entry<DayPeriod, Set<Span>> entry : dayPeriodsToSpans.keyValuesSet()) {
    197             DayPeriod dayPeriod = entry.getKey();
    198             Set<Span> spanSet = entry.getValue();
    199             if (spanSet.size() > 0) {
    200                 for (Span span : spanSet) {
    201                     int start = span.start % NOON;
    202                     int end = span.getAdjustedEnd() % NOON;
    203                     for (Span span2 : spanSet) {
    204                         if (span2 == span) {
    205                             continue;
    206                         }
    207                         // if there is overlap when mapped to 12 hours...
    208                         int start2 = span2.start % NOON;
    209                         int end2 = span2.getAdjustedEnd() % NOON;
    210                         // disjoint if e1 < s2 || e2 < s1
    211                         if (start >= end2 && start2 >= end) {
    212                             throw new IllegalArgumentException("Overlapping times for " + dayPeriod + ":\t"
    213                                 + span + "\t" + span2 + "\t" + Arrays.asList(locales));
    214                         }
    215                     }
    216                 }
    217             }
    218         }
    219     }
    220 
    221     /**
    222      * Return the start (in millis) of the first matching day period, or -1 if no match,
    223      *
    224      * @param dayPeriod
    225      * @return seconds in day
    226      */
    227     public int getFirstStartTime(DayPeriodInfo.DayPeriod dayPeriod) {
    228         for (int i = 0; i < spans.length; ++i) {
    229             if (spans[i].dayPeriod == dayPeriod) {
    230                 return spans[i].start;
    231             }
    232         }
    233         return -1;
    234     }
    235 
    236     /**
    237      * Return the start, end, and whether the start is included.
    238      *
    239      * @param dayPeriod
    240      * @return start,end,includesStart,period
    241      */
    242     public R3<Integer, Integer, Boolean> getFirstDayPeriodInfo(DayPeriodInfo.DayPeriod dayPeriod) {
    243         Span span = getFirstDayPeriodSpan(dayPeriod);
    244         return Row.of(span.start, span.end, true);
    245     }
    246 
    247     public Span getFirstDayPeriodSpan(DayPeriodInfo.DayPeriod dayPeriod) {
    248         switch (dayPeriod) {
    249         case am:
    250             return DayPeriod.am.span;
    251         case pm:
    252             return DayPeriod.pm.span;
    253         default:
    254             Set<Span> spanList = dayPeriodsToSpans.get(dayPeriod);
    255             return spanList == null ? null : dayPeriodsToSpans.get(dayPeriod).iterator().next();
    256         }
    257     }
    258 
    259     public Set<Span> getDayPeriodSpans(DayPeriodInfo.DayPeriod dayPeriod) {
    260         switch (dayPeriod) {
    261         case am:
    262             return Collections.singleton(DayPeriod.am.span);
    263         case pm:
    264             return Collections.singleton(DayPeriod.pm.span);
    265         default:
    266             return dayPeriodsToSpans.get(dayPeriod);
    267         }
    268     }
    269 
    270     /**
    271      * Returns the day period for the time.
    272      *
    273      * @param millisInDay
    274      *            If not (millisInDay > 0 && The millisInDay < DAY_LIMIT) throws exception.
    275      * @return corresponding day period
    276      */
    277     public DayPeriodInfo.DayPeriod getDayPeriod(int millisInDay) {
    278         if (millisInDay < MIDNIGHT) {
    279             throw new IllegalArgumentException("millisInDay too small");
    280         } else if (millisInDay >= DAY_LIMIT) {
    281             throw new IllegalArgumentException("millisInDay too big");
    282         }
    283         for (int i = 0; i < spans.length; ++i) {
    284             if (spans[i].contains(millisInDay)) {
    285                 return spans[i].dayPeriod;
    286             }
    287         }
    288         throw new IllegalArgumentException("internal error");
    289     }
    290 
    291     /**
    292      * Returns the number of periods in the day
    293      *
    294      * @return
    295      */
    296     public int getPeriodCount() {
    297         return spans.length;
    298     }
    299 
    300     /**
    301      * For the nth period in the day, returns the start, whether the start is included, and the period ID.
    302      *
    303      * @param index
    304      * @return data
    305      */
    306     public Row.R3<Integer, Boolean, DayPeriod> getPeriod(int index) {
    307         return Row.of(getSpan(index).start, true, getSpan(index).dayPeriod);
    308     }
    309 
    310     public Span getSpan(int index) {
    311         return spans[index];
    312     }
    313 
    314     public List<DayPeriod> getPeriods() {
    315         return Arrays.asList(dayPeriods);
    316     }
    317 
    318     @Override
    319     public String toString() {
    320         return dayPeriodsToSpans.values().toString();
    321     }
    322 
    323     public String toString(DayPeriod dayPeriod) {
    324         switch (dayPeriod) {
    325         case midnight:
    326             return "00:00";
    327         case noon:
    328             return "12:00";
    329         case am:
    330             return "00:00  12:00";
    331         case pm:
    332             return "12:00  24:00";
    333         default:
    334             break;
    335         }
    336         StringBuilder result = new StringBuilder();
    337         for (Span span : dayPeriodsToSpans.get(dayPeriod)) {
    338             if (result.length() != 0) {
    339                 result.append("; ");
    340             }
    341             result.append(span.toStringPlain());
    342         }
    343         return result.toString();
    344     }
    345 
    346     public static String formatTime(int time) {
    347         int minutes = time / (60 * 1000);
    348         int hours = minutes / 60;
    349         minutes -= hours * 60;
    350         return String.format("%02d:%02d", hours, minutes);
    351     }
    352 
    353     // Day periods that are allowed to collide
    354     private static final EnumMap<DayPeriod, EnumSet<DayPeriod>> allowableCollisions = new EnumMap<DayPeriod, EnumSet<DayPeriod>>(DayPeriod.class);
    355     static {
    356         allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2));
    357         allowableCollisions.put(DayPeriod.pm, EnumSet.of(DayPeriod.afternoon1, DayPeriod.afternoon2, DayPeriod.evening1, DayPeriod.evening2));
    358     }
    359 
    360     /**
    361      * Test if there is a problem with dayPeriod1 and dayPeriod2 having the same localization.
    362      * @param type1
    363      * @param dayPeriod1
    364      * @param type2 TODO
    365      * @param dayPeriod2
    366      * @param sampleError TODO
    367      * @return
    368      */
    369     public boolean collisionIsError(DayPeriodInfo.Type type1, DayPeriod dayPeriod1, Type type2, DayPeriod dayPeriod2,
    370         Output<Integer> sampleError) {
    371         if (dayPeriod1 == dayPeriod2) {
    372             return false;
    373         }
    374         if ((allowableCollisions.containsKey(dayPeriod1) && allowableCollisions.get(dayPeriod1).contains(dayPeriod2)) ||
    375             (allowableCollisions.containsKey(dayPeriod2) && allowableCollisions.get(dayPeriod2).contains(dayPeriod1))) {
    376             return false;
    377         }
    378 
    379         // we use the more lenient if they are mixed types
    380         if (type2 == Type.format) {
    381             type1 = Type.format;
    382         }
    383 
    384         // At this point, they are unequal
    385         // The fixed cannot overlap among themselves
    386         final boolean fixed1 = dayPeriod1.isFixed();
    387         final boolean fixed2 = dayPeriod2.isFixed();
    388         if (fixed1 && fixed2) {
    389             return true;
    390         }
    391         // at this point, at least one is flexible.
    392         // make sure the second is not flexible.
    393         DayPeriod fixedOrFlexible;
    394         DayPeriod flexible;
    395         if (fixed1) {
    396             fixedOrFlexible = dayPeriod1;
    397             flexible = dayPeriod2;
    398         } else {
    399             fixedOrFlexible = dayPeriod2;
    400             flexible = dayPeriod1;
    401         }
    402 
    403         // TODO since periods are sorted, could optimize further
    404 
    405         switch (type1) {
    406         case format: {
    407             if (fixedOrFlexible.span != null) {
    408                 if (collisionIsErrorFormat(flexible, fixedOrFlexible.span, sampleError)) {
    409                     return true;
    410                 }
    411             } else { // flexible
    412                 for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
    413                     if (collisionIsErrorFormat(flexible, s, sampleError)) {
    414                         return true;
    415                     }
    416                 }
    417             }
    418             break;
    419         }
    420         case selection: {
    421             if (fixedOrFlexible.span != null) {
    422                 if (collisionIsErrorSelection(flexible, fixedOrFlexible.span, sampleError)) {
    423                     return true;
    424                 }
    425             } else { // flexible
    426                 for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
    427                     if (collisionIsErrorSelection(flexible, s, sampleError)) {
    428                         return true;
    429                     }
    430                 }
    431             }
    432             break;
    433         }
    434         }
    435         return false; // no bad collision
    436     }
    437 
    438     // Formatting has looser collision rules, because it is always paired with a time.
    439     // That is, it is not a problem if two items collide,
    440     // if it doesn't cause a collision when paired with a time.
    441     // But if 11:00 has the same format (eg 11 X) as 23:00, there IS a collision.
    442     // So we see if there is an overlap mod 12.
    443     private boolean collisionIsErrorFormat(DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
    444         int otherStart = other.start % NOON;
    445         int otherEnd = other.getAdjustedEnd() % NOON;
    446         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
    447             int flexStart = s.start % NOON;
    448             int flexEnd = s.getAdjustedEnd() % NOON;
    449             if (otherStart <= flexEnd && otherEnd >= flexStart) { // overlap?
    450                 if (sampleError != null) {
    451                     sampleError.value = Math.max(otherStart, otherEnd);
    452                 }
    453                 return true;
    454             }
    455         }
    456         return false;
    457     }
    458 
    459     // Selection has stricter collision rules, because is is used to select different messages.
    460     // So two types with the same localization do collide unless they have exactly the same rules.
    461     private boolean collisionIsErrorSelection(DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
    462         int otherStart = other.start;
    463         int otherEnd = other.getAdjustedEnd();
    464         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
    465             int flexStart = s.start;
    466             int flexEnd = s.getAdjustedEnd();
    467             if (otherStart != flexStart) { // not same??
    468                 if (sampleError != null) {
    469                     sampleError.value = (otherStart + flexStart) / 2; // half-way between
    470                 }
    471                 return true;
    472             } else if (otherEnd != flexEnd) { // not same??
    473                 if (sampleError != null) {
    474                     sampleError.value = (otherEnd + flexEnd) / 2; // half-way between
    475                 }
    476                 return true;
    477             }
    478         }
    479         return false;
    480     }
    481 }