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         if (span == null) {
245             return null;
246         }
247         return Row.of(span.start, span.end, true);
248     }
249 
getFirstDayPeriodSpan(DayPeriodInfo.DayPeriod dayPeriod)250     public Span getFirstDayPeriodSpan(DayPeriodInfo.DayPeriod dayPeriod) {
251         switch (dayPeriod) {
252         case am:
253             return DayPeriod.am.span;
254         case pm:
255             return DayPeriod.pm.span;
256         default:
257             Set<Span> spanList = dayPeriodsToSpans.get(dayPeriod);
258             return spanList == null ? null : dayPeriodsToSpans.get(dayPeriod).iterator().next();
259         }
260     }
261 
getDayPeriodSpans(DayPeriodInfo.DayPeriod dayPeriod)262     public Set<Span> getDayPeriodSpans(DayPeriodInfo.DayPeriod dayPeriod) {
263         switch (dayPeriod) {
264         case am:
265             return Collections.singleton(DayPeriod.am.span);
266         case pm:
267             return Collections.singleton(DayPeriod.pm.span);
268         default:
269             return dayPeriodsToSpans.get(dayPeriod);
270         }
271     }
272 
273     /**
274      * Returns the day period for the time.
275      *
276      * @param millisInDay
277      *            If not (millisInDay > 0 && The millisInDay < DAY_LIMIT) throws exception.
278      * @return corresponding day period
279      */
getDayPeriod(int millisInDay)280     public DayPeriodInfo.DayPeriod getDayPeriod(int millisInDay) {
281         if (millisInDay < MIDNIGHT) {
282             throw new IllegalArgumentException("millisInDay too small");
283         } else if (millisInDay >= DAY_LIMIT) {
284             throw new IllegalArgumentException("millisInDay too big");
285         }
286         for (int i = 0; i < spans.length; ++i) {
287             if (spans[i].contains(millisInDay)) {
288                 return spans[i].dayPeriod;
289             }
290         }
291         throw new IllegalArgumentException("internal error");
292     }
293 
294     /**
295      * Returns the number of periods in the day
296      *
297      * @return
298      */
getPeriodCount()299     public int getPeriodCount() {
300         return spans.length;
301     }
302 
303     /**
304      * For the nth period in the day, returns the start, whether the start is included, and the period ID.
305      *
306      * @param index
307      * @return data
308      */
getPeriod(int index)309     public Row.R3<Integer, Boolean, DayPeriod> getPeriod(int index) {
310         return Row.of(getSpan(index).start, true, getSpan(index).dayPeriod);
311     }
312 
getSpan(int index)313     public Span getSpan(int index) {
314         return spans[index];
315     }
316 
getPeriods()317     public List<DayPeriod> getPeriods() {
318         return Arrays.asList(dayPeriods);
319     }
320 
321     @Override
toString()322     public String toString() {
323         return dayPeriodsToSpans.values().toString();
324     }
325 
toString(DayPeriod dayPeriod)326     public String toString(DayPeriod dayPeriod) {
327         switch (dayPeriod) {
328         case midnight:
329             return "00:00";
330         case noon:
331             return "12:00";
332         case am:
333             return "00:00 – 12:00⁻";
334         case pm:
335             return "12:00 – 24:00⁻";
336         default:
337             break;
338         }
339         StringBuilder result = new StringBuilder();
340         Set<Span> set = dayPeriodsToSpans.get(dayPeriod);
341         if (set != null) {
342             for (Span span : set) {
343                 if (span != null) {
344                     if (result.length() != 0) {
345                         result.append("; ");
346                     }
347                     result.append(span.toStringPlain());
348                 }
349             }
350         }
351         return result.toString();
352     }
353 
formatTime(int time)354     public static String formatTime(int time) {
355         int minutes = time / (60 * 1000);
356         int hours = minutes / 60;
357         minutes -= hours * 60;
358         return String.format("%02d:%02d", hours, minutes);
359     }
360 
361     // Day periods that are allowed to collide
362     private static final EnumMap<DayPeriod, EnumSet<DayPeriod>> allowableCollisions = new EnumMap<>(DayPeriod.class);
363     static {
allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2))364         allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2));
allowableCollisions.put(DayPeriod.pm, EnumSet.of(DayPeriod.afternoon1, DayPeriod.afternoon2, DayPeriod.evening1, DayPeriod.evening2))365         allowableCollisions.put(DayPeriod.pm, EnumSet.of(DayPeriod.afternoon1, DayPeriod.afternoon2, DayPeriod.evening1, DayPeriod.evening2));
366     }
367 
368     /**
369      * Test if there is a problem with dayPeriod1 and dayPeriod2 having the same localization.
370      * @param type1
371      * @param dayPeriod1
372      * @param type2 TODO
373      * @param dayPeriod2
374      * @param sampleError TODO
375      * @return
376      */
collisionIsError(DayPeriodInfo.Type type1, DayPeriod dayPeriod1, Type type2, DayPeriod dayPeriod2, Output<Integer> sampleError)377     public boolean collisionIsError(DayPeriodInfo.Type type1, DayPeriod dayPeriod1, Type type2, DayPeriod dayPeriod2,
378         Output<Integer> sampleError) {
379         if (dayPeriod1 == dayPeriod2) {
380             return false;
381         }
382         if ((allowableCollisions.containsKey(dayPeriod1) && allowableCollisions.get(dayPeriod1).contains(dayPeriod2)) ||
383             (allowableCollisions.containsKey(dayPeriod2) && allowableCollisions.get(dayPeriod2).contains(dayPeriod1))) {
384             return false;
385         }
386 
387         // we use the more lenient if they are mixed types
388         if (type2 == Type.format) {
389             type1 = Type.format;
390         }
391 
392         // At this point, they are unequal
393         // The fixed cannot overlap among themselves
394         final boolean fixed1 = dayPeriod1.isFixed();
395         final boolean fixed2 = dayPeriod2.isFixed();
396         if (fixed1 && fixed2) {
397             return true;
398         }
399         // at this point, at least one is flexible.
400         // make sure the second is not flexible.
401         DayPeriod fixedOrFlexible;
402         DayPeriod flexible;
403         if (fixed1) {
404             fixedOrFlexible = dayPeriod1;
405             flexible = dayPeriod2;
406         } else {
407             fixedOrFlexible = dayPeriod2;
408             flexible = dayPeriod1;
409         }
410 
411         // TODO since periods are sorted, could optimize further
412 
413         switch (type1) {
414         case format: {
415             if (fixedOrFlexible.span != null) {
416                 if (collisionIsErrorFormat(flexible, fixedOrFlexible.span, sampleError)) {
417                     return true;
418                 }
419             } else { // flexible
420                 for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
421                     if (collisionIsErrorFormat(flexible, s, sampleError)) {
422                         return true;
423                     }
424                 }
425             }
426             break;
427         }
428         case selection: {
429             if (fixedOrFlexible.span != null) {
430                 if (collisionIsErrorSelection(flexible, fixedOrFlexible.span, sampleError)) {
431                     return true;
432                 }
433             } else { // flexible
434                 for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
435                     if (collisionIsErrorSelection(flexible, s, sampleError)) {
436                         return true;
437                     }
438                 }
439             }
440             break;
441         }
442         }
443         return false; // no bad collision
444     }
445 
446     // Formatting has looser collision rules, because it is always paired with a time.
447     // That is, it is not a problem if two items collide,
448     // if it doesn't cause a collision when paired with a time.
449     // But if 11:00 has the same format (eg 11 X) as 23:00, there IS a collision.
450     // So we see if there is an overlap mod 12.
collisionIsErrorFormat(DayPeriod dayPeriod, Span other, Output<Integer> sampleError)451     private boolean collisionIsErrorFormat(DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
452         int otherStart = other.start % NOON;
453         int otherEnd = other.getAdjustedEnd() % NOON;
454         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
455             int flexStart = s.start % NOON;
456             int flexEnd = s.getAdjustedEnd() % NOON;
457             if (otherStart <= flexEnd && otherEnd >= flexStart) { // overlap?
458                 if (sampleError != null) {
459                     sampleError.value = Math.max(otherStart, otherEnd);
460                 }
461                 return true;
462             }
463         }
464         return false;
465     }
466 
467     // Selection has stricter collision rules, because is is used to select different messages.
468     // So two types with the same localization do collide unless they have exactly the same rules.
collisionIsErrorSelection(DayPeriod dayPeriod, Span other, Output<Integer> sampleError)469     private boolean collisionIsErrorSelection(DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
470         int otherStart = other.start;
471         int otherEnd = other.getAdjustedEnd();
472         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
473             int flexStart = s.start;
474             int flexEnd = s.getAdjustedEnd();
475             if (otherStart != flexStart) { // not same??
476                 if (sampleError != null) {
477                     sampleError.value = (otherStart + flexStart) / 2; // half-way between
478                 }
479                 return true;
480             } else if (otherEnd != flexEnd) { // not same??
481                 if (sampleError != null) {
482                     sampleError.value = (otherEnd + flexEnd) / 2; // half-way between
483                 }
484                 return true;
485             }
486         }
487         return false;
488     }
489 }