• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Type(String _pathValue)29         private Type(String _pathValue) {
30             pathValue = _pathValue;
31         }
32 
fromString(String source)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 
Span(int start, int end, DayPeriod dayPeriod)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
compareTo(Span o)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 
contains(int millisInDay)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          */
getAdjustedEnd()73         public int getAdjustedEnd() {
74             return includesEnd ? end : end - 1;
75         }
76 
77         @Override
equals(Object obj)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
hashCode()85         public int hashCode() {
86             return start * 37 + end;
87         }
88 
89         @Override
toString()90         public String toString() {
91             return dayPeriod + ":" + toStringPlain();
92         }
93 
toStringPlain()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 
DayPeriod(int start, int end)107         private DayPeriod(int start, int end) {
108             span = new Span(start, end, this);
109         }
110 
DayPeriod()111         private DayPeriod() {
112             span = null;
113         }
114 
fromString(String value)115         public static DayPeriod fromString(String value) {
116             return valueOf(value);
117         }
118 
isFixed()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 
add(DayPeriodInfo.DayPeriod dayPeriod, int start, boolean includesStart, int end, boolean includesEnd)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 
finish(String[] locales)149         public DayPeriodInfo finish(String[] locales) {
150             DayPeriodInfo result = new DayPeriodInfo(info, locales);
151             info.clear();
152             return result;
153         }
154 
contains(DayPeriod dayPeriod)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 
DayPeriodInfo(TreeSet<Span> info, String[] locales)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      */
getFirstStartTime(DayPeriodInfo.DayPeriod dayPeriod)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      */
getFirstDayPeriodInfo(DayPeriodInfo.DayPeriod dayPeriod)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 
getFirstDayPeriodSpan(DayPeriodInfo.DayPeriod dayPeriod)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 
getDayPeriodSpans(DayPeriodInfo.DayPeriod dayPeriod)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      */
getDayPeriod(int millisInDay)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      */
getPeriodCount()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      */
getPeriod(int index)306     public Row.R3<Integer, Boolean, DayPeriod> getPeriod(int index) {
307         return Row.of(getSpan(index).start, true, getSpan(index).dayPeriod);
308     }
309 
getSpan(int index)310     public Span getSpan(int index) {
311         return spans[index];
312     }
313 
getPeriods()314     public List<DayPeriod> getPeriods() {
315         return Arrays.asList(dayPeriods);
316     }
317 
318     @Override
toString()319     public String toString() {
320         return dayPeriodsToSpans.values().toString();
321     }
322 
toString(DayPeriod dayPeriod)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 
formatTime(int time)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 {
allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2))356         allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2));
allowableCollisions.put(DayPeriod.pm, EnumSet.of(DayPeriod.afternoon1, DayPeriod.afternoon2, DayPeriod.evening1, DayPeriod.evening2))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      */
collisionIsError(DayPeriodInfo.Type type1, DayPeriod dayPeriod1, Type type2, DayPeriod dayPeriod2, Output<Integer> sampleError)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.
collisionIsErrorFormat(DayPeriod dayPeriod, Span other, Output<Integer> sampleError)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.
collisionIsErrorSelection(DayPeriod dayPeriod, Span other, Output<Integer> sampleError)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 }