• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.util;
2 
3 import com.ibm.icu.impl.Relation;
4 import com.ibm.icu.impl.Row;
5 import com.ibm.icu.impl.Row.R3;
6 import com.ibm.icu.util.Output;
7 import java.util.ArrayList;
8 import java.util.Arrays;
9 import java.util.Collections;
10 import java.util.EnumMap;
11 import java.util.EnumSet;
12 import java.util.LinkedHashSet;
13 import java.util.List;
14 import java.util.Map.Entry;
15 import java.util.Set;
16 import java.util.TreeSet;
17 
18 public class DayPeriodInfo {
19     public static final int HOUR = 60 * 60 * 1000;
20     public static final int MIDNIGHT = 0;
21     public static final int NOON = 12 * HOUR;
22     public static final int DAY_LIMIT = 24 * HOUR;
23 
24     public enum Type {
25         format("format"),
26         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          *
72          * @return
73          */
getAdjustedEnd()74         public int getAdjustedEnd() {
75             return includesEnd ? end : end - 1;
76         }
77 
78         @Override
equals(Object obj)79         public boolean equals(Object obj) {
80             Span other = (Span) obj;
81             return start == other.start && end == other.end;
82             // because includesEnd is determined by the above, we're done
83         }
84 
85         @Override
hashCode()86         public int hashCode() {
87             return start * 37 + end;
88         }
89 
90         @Override
toString()91         public String toString() {
92             return dayPeriod + ":" + toStringPlain();
93         }
94 
toStringPlain()95         public String toStringPlain() {
96             return formatTime(start) + " – " + formatTime(end) + (includesEnd ? "" : "⁻");
97         }
98     }
99 
100     public enum DayPeriod {
101         // fixed
102         midnight(MIDNIGHT, MIDNIGHT),
103         am(MIDNIGHT, NOON),
104         noon(NOON, NOON),
105         pm(NOON, DAY_LIMIT),
106         // flexible
107         morning1,
108         morning2,
109         afternoon1,
110         afternoon2,
111         evening1,
112         evening2,
113         night1,
114         night2;
115 
116         public final Span span;
117 
DayPeriod(int start, int end)118         private DayPeriod(int start, int end) {
119             span = new Span(start, end, this);
120         }
121 
DayPeriod()122         private DayPeriod() {
123             span = null;
124         }
125 
fromString(String value)126         public static DayPeriod fromString(String value) {
127             return valueOf(value);
128         }
129 
isFixed()130         public boolean isFixed() {
131             return span != null;
132         }
133     }
134 
135     // the arrays must be in sorted order. First must have start= zero. Last must have end =
136     // DAY_LIMIT (and !includesEnd)
137     // each of these will have the same length, and correspond.
138     private final Span[] spans;
139     private final DayPeriodInfo.DayPeriod[] dayPeriods;
140     final Relation<DayPeriod, Span> dayPeriodsToSpans =
141             Relation.of(new EnumMap<DayPeriod, Set<Span>>(DayPeriod.class), LinkedHashSet.class);
142 
143     public static class Builder {
144         TreeSet<Span> info = new TreeSet<>();
145 
146         // TODO add rule test that they can't span same 12 hour time.
147 
add( DayPeriodInfo.DayPeriod dayPeriod, int start, boolean includesStart, int end, boolean includesEnd)148         public DayPeriodInfo.Builder add(
149                 DayPeriodInfo.DayPeriod dayPeriod,
150                 int start,
151                 boolean includesStart,
152                 int end,
153                 boolean includesEnd) {
154             if (dayPeriod == null
155                     || start < 0
156                     || start > end
157                     || end > DAY_LIMIT
158                     || end - start > NOON) { // the span can't exceed 12 hours
159                 throw new IllegalArgumentException("Bad data");
160             }
161             Span span = new Span(start, end, dayPeriod);
162             boolean didntContain = info.add(span);
163             if (!didntContain) {
164                 throw new IllegalArgumentException("Duplicate data: " + span);
165             }
166             return this;
167         }
168 
finish(String[] locales)169         public DayPeriodInfo finish(String[] locales) {
170             DayPeriodInfo result = new DayPeriodInfo(info, locales);
171             info.clear();
172             return result;
173         }
174 
contains(DayPeriod dayPeriod)175         public boolean contains(DayPeriod dayPeriod) {
176             for (Span span : info) {
177                 if (span.dayPeriod == dayPeriod) {
178                     return true;
179                 }
180             }
181             return false;
182         }
183     }
184 
DayPeriodInfo(TreeSet<Span> info, String[] locales)185     private DayPeriodInfo(TreeSet<Span> info, String[] locales) {
186         int len = info.size();
187         spans = info.toArray(new Span[len]);
188         List<DayPeriod> tempPeriods = new ArrayList<>();
189         // check data
190         if (len != 0) {
191             Span last = spans[0];
192             tempPeriods.add(last.dayPeriod);
193             dayPeriodsToSpans.put(last.dayPeriod, last);
194             if (last.start != MIDNIGHT) {
195                 throw new IllegalArgumentException("Doesn't start at 0:00).");
196             }
197             for (int i = 1; i < len; ++i) {
198                 Span current = spans[i];
199                 if (current.start != current.end && last.start != last.end) {
200                     if (current.start != last.end) {
201                         throw new IllegalArgumentException(
202                                 "Gap or overlapping times:\t"
203                                         + current
204                                         + "\t"
205                                         + last
206                                         + "\t"
207                                         + Arrays.asList(locales));
208                     }
209                 }
210                 tempPeriods.add(current.dayPeriod);
211                 dayPeriodsToSpans.put(current.dayPeriod, current);
212                 last = current;
213             }
214             if (last.end != DAY_LIMIT) {
215                 throw new IllegalArgumentException("Doesn't reach 24:00).");
216             }
217         }
218         dayPeriods = tempPeriods.toArray(new DayPeriod[len]);
219         dayPeriodsToSpans.freeze();
220         // add an extra check to make sure that periods are unique over 12 hour spans
221         for (Entry<DayPeriod, Set<Span>> entry : dayPeriodsToSpans.keyValuesSet()) {
222             DayPeriod dayPeriod = entry.getKey();
223             Set<Span> spanSet = entry.getValue();
224             if (spanSet.size() > 0) {
225                 for (Span span : spanSet) {
226                     int start = span.start % NOON;
227                     int end = span.getAdjustedEnd() % NOON;
228                     for (Span span2 : spanSet) {
229                         if (span2 == span) {
230                             continue;
231                         }
232                         // if there is overlap when mapped to 12 hours...
233                         int start2 = span2.start % NOON;
234                         int end2 = span2.getAdjustedEnd() % NOON;
235                         // disjoint if e1 < s2 || e2 < s1
236                         if (start >= end2 && start2 >= end) {
237                             throw new IllegalArgumentException(
238                                     "Overlapping times for "
239                                             + dayPeriod
240                                             + ":\t"
241                                             + span
242                                             + "\t"
243                                             + span2
244                                             + "\t"
245                                             + Arrays.asList(locales));
246                         }
247                     }
248                 }
249             }
250         }
251     }
252 
253     /**
254      * Return the start (in millis) of the first matching day period, or -1 if no match,
255      *
256      * @param dayPeriod
257      * @return seconds in day
258      */
getFirstStartTime(DayPeriodInfo.DayPeriod dayPeriod)259     public int getFirstStartTime(DayPeriodInfo.DayPeriod dayPeriod) {
260         for (int i = 0; i < spans.length; ++i) {
261             if (spans[i].dayPeriod == dayPeriod) {
262                 return spans[i].start;
263             }
264         }
265         return -1;
266     }
267 
268     /**
269      * Return the start, end, and whether the start is included.
270      *
271      * @param dayPeriod
272      * @return start,end,includesStart,period
273      */
getFirstDayPeriodInfo(DayPeriodInfo.DayPeriod dayPeriod)274     public R3<Integer, Integer, Boolean> getFirstDayPeriodInfo(DayPeriodInfo.DayPeriod dayPeriod) {
275         Span span = getFirstDayPeriodSpan(dayPeriod);
276         if (span == null) {
277             return null;
278         }
279         return Row.of(span.start, span.end, true);
280     }
281 
getFirstDayPeriodSpan(DayPeriodInfo.DayPeriod dayPeriod)282     public Span getFirstDayPeriodSpan(DayPeriodInfo.DayPeriod dayPeriod) {
283         switch (dayPeriod) {
284             case am:
285                 return DayPeriod.am.span;
286             case pm:
287                 return DayPeriod.pm.span;
288             default:
289                 Set<Span> spanList = dayPeriodsToSpans.get(dayPeriod);
290                 return spanList == null ? null : dayPeriodsToSpans.get(dayPeriod).iterator().next();
291         }
292     }
293 
getDayPeriodSpans(DayPeriodInfo.DayPeriod dayPeriod)294     public Set<Span> getDayPeriodSpans(DayPeriodInfo.DayPeriod dayPeriod) {
295         switch (dayPeriod) {
296             case am:
297                 return Collections.singleton(DayPeriod.am.span);
298             case pm:
299                 return Collections.singleton(DayPeriod.pm.span);
300             default:
301                 return dayPeriodsToSpans.get(dayPeriod);
302         }
303     }
304 
305     /**
306      * Returns the day period for the time.
307      *
308      * @param millisInDay If not (millisInDay > 0 && The millisInDay < DAY_LIMIT) throws exception.
309      * @return corresponding day period
310      */
getDayPeriod(int millisInDay)311     public DayPeriodInfo.DayPeriod getDayPeriod(int millisInDay) {
312         if (millisInDay < MIDNIGHT) {
313             throw new IllegalArgumentException("millisInDay too small");
314         } else if (millisInDay >= DAY_LIMIT) {
315             throw new IllegalArgumentException("millisInDay too big");
316         }
317         for (int i = 0; i < spans.length; ++i) {
318             if (spans[i].contains(millisInDay)) {
319                 return spans[i].dayPeriod;
320             }
321         }
322         throw new IllegalArgumentException("internal error");
323     }
324 
325     /**
326      * Returns the number of periods in the day
327      *
328      * @return
329      */
getPeriodCount()330     public int getPeriodCount() {
331         return spans.length;
332     }
333 
334     /**
335      * For the nth period in the day, returns the start, whether the start is included, and the
336      * period ID.
337      *
338      * @param index
339      * @return data
340      */
getPeriod(int index)341     public Row.R3<Integer, Boolean, DayPeriod> getPeriod(int index) {
342         return Row.of(getSpan(index).start, true, getSpan(index).dayPeriod);
343     }
344 
getSpan(int index)345     public Span getSpan(int index) {
346         return spans[index];
347     }
348 
getPeriods()349     public List<DayPeriod> getPeriods() {
350         return Arrays.asList(dayPeriods);
351     }
352 
353     @Override
toString()354     public String toString() {
355         return dayPeriodsToSpans.values().toString();
356     }
357 
toString(DayPeriod dayPeriod)358     public String toString(DayPeriod dayPeriod) {
359         switch (dayPeriod) {
360             case midnight:
361                 return "00:00";
362             case noon:
363                 return "12:00";
364             case am:
365                 return "00:00 – 12:00⁻";
366             case pm:
367                 return "12:00 – 24:00⁻";
368             default:
369                 break;
370         }
371         StringBuilder result = new StringBuilder();
372         Set<Span> set = dayPeriodsToSpans.get(dayPeriod);
373         if (set != null) {
374             for (Span span : set) {
375                 if (span != null) {
376                     if (result.length() != 0) {
377                         result.append("; ");
378                     }
379                     result.append(span.toStringPlain());
380                 }
381             }
382         }
383         return result.toString();
384     }
385 
formatTime(long time)386     public static String formatTime(long time) {
387         long minutes = time / (60 * 1000);
388         long hours = minutes / 60;
389         minutes -= hours * 60;
390         return String.format("%02d:%02d", hours, minutes);
391     }
392 
393     // Day periods that are allowed to collide
394     private static final EnumMap<DayPeriod, EnumSet<DayPeriod>> allowableCollisions =
395             new EnumMap<>(DayPeriod.class);
396 
397     static {
allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2))398         allowableCollisions.put(DayPeriod.am, EnumSet.of(DayPeriod.morning1, DayPeriod.morning2));
allowableCollisions.put( DayPeriod.pm, EnumSet.of( DayPeriod.afternoon1, DayPeriod.afternoon2, DayPeriod.evening1, DayPeriod.evening2))399         allowableCollisions.put(
400                 DayPeriod.pm,
401                 EnumSet.of(
402                         DayPeriod.afternoon1,
403                         DayPeriod.afternoon2,
404                         DayPeriod.evening1,
405                         DayPeriod.evening2));
406     }
407 
408     /**
409      * Test if there is a problem with dayPeriod1 and dayPeriod2 having the same localization.
410      *
411      * @param type1
412      * @param dayPeriod1
413      * @param type2 TODO
414      * @param dayPeriod2
415      * @param sampleError TODO
416      * @return
417      */
collisionIsError( DayPeriodInfo.Type type1, DayPeriod dayPeriod1, Type type2, DayPeriod dayPeriod2, Output<Integer> sampleError)418     public boolean collisionIsError(
419             DayPeriodInfo.Type type1,
420             DayPeriod dayPeriod1,
421             Type type2,
422             DayPeriod dayPeriod2,
423             Output<Integer> sampleError) {
424         if (dayPeriod1 == dayPeriod2) {
425             return false;
426         }
427         if ((allowableCollisions.containsKey(dayPeriod1)
428                         && allowableCollisions.get(dayPeriod1).contains(dayPeriod2))
429                 || (allowableCollisions.containsKey(dayPeriod2)
430                         && allowableCollisions.get(dayPeriod2).contains(dayPeriod1))) {
431             return false;
432         }
433 
434         // Hack for French night1, CLDR-17132 for better fix
435         // Let night1 have the same name as morning1/am if night1 starts at 00:00
436         if ((dayPeriod1 == DayPeriod.night1
437                         && (dayPeriod2 == DayPeriod.morning1 || dayPeriod2 == DayPeriod.am))
438                 || (dayPeriod2 == DayPeriod.night1
439                         && (dayPeriod1 == DayPeriod.morning1 || dayPeriod1 == DayPeriod.am))) {
440             if (dayPeriodsToSpans.get(DayPeriod.night1).size() == 1) {
441                 for (Span s : dayPeriodsToSpans.get(DayPeriod.night1)) {
442                     if (s.start == MIDNIGHT) {
443                         return false;
444                     }
445                 }
446             }
447         }
448 
449         // Hack for fil evening1/night1, CLDR-17139 for better fix
450         // Let night1 have the same name as evening1 if night1 ends at 24:00
451         if ((dayPeriod1 == DayPeriod.night1 && dayPeriod2 == DayPeriod.evening1)
452                 || (dayPeriod2 == DayPeriod.night1 && dayPeriod1 == DayPeriod.evening1)) {
453             if (dayPeriodsToSpans.get(DayPeriod.night1).size() == 1) {
454                 for (Span s : dayPeriodsToSpans.get(DayPeriod.night1)) {
455                     if (s.end == DAY_LIMIT) {
456                         return false;
457                     }
458                 }
459             }
460         }
461 
462         // Fix for CLDR-16933, for format types only
463         // Let midnight match a non-fixed period that starts at, ends at, or contains midnight (both
464         // versions);
465         // Let noon match a non-fixed period that starts at, ends at, or contains noon (or just
466         // before noon);
467         if (type1 == Type.format && type2 == Type.format) {
468             if (dayPeriod1 == DayPeriod.midnight && !dayPeriod2.isFixed()) {
469                 for (Span s : dayPeriodsToSpans.get(dayPeriod2)) {
470                     if (s.contains(MIDNIGHT) || s.contains(DAY_LIMIT)) {
471                         return false;
472                     }
473                 }
474             }
475             if (dayPeriod2 == DayPeriod.midnight && !dayPeriod1.isFixed()) {
476                 for (Span s : dayPeriodsToSpans.get(dayPeriod1)) {
477                     if (s.contains(MIDNIGHT) || s.contains(DAY_LIMIT)) {
478                         return false;
479                     }
480                 }
481             }
482             if (dayPeriod1 == DayPeriod.noon && !dayPeriod2.isFixed()) {
483                 for (Span s : dayPeriodsToSpans.get(dayPeriod2)) {
484                     if (s.contains(NOON) || s.contains(NOON - 1)) {
485                         return false;
486                     }
487                 }
488             }
489             if (dayPeriod2 == DayPeriod.noon && !dayPeriod1.isFixed()) {
490                 for (Span s : dayPeriodsToSpans.get(dayPeriod1)) {
491                     if (s.contains(NOON) || s.contains(NOON - 1)) {
492                         return false;
493                     }
494                 }
495             }
496         }
497 
498         // we use the more lenient if they are mixed types
499         if (type2 == Type.format) {
500             type1 = Type.format;
501         }
502 
503         // At this point, they are unequal
504         // The fixed cannot overlap among themselves
505         final boolean fixed1 = dayPeriod1.isFixed();
506         final boolean fixed2 = dayPeriod2.isFixed();
507         if (fixed1 && fixed2) {
508             return true;
509         }
510         // at this point, at least one is flexible.
511         // make sure the second is not flexible.
512         DayPeriod fixedOrFlexible;
513         DayPeriod flexible;
514         if (fixed1) {
515             fixedOrFlexible = dayPeriod1;
516             flexible = dayPeriod2;
517         } else {
518             fixedOrFlexible = dayPeriod2;
519             flexible = dayPeriod1;
520         }
521 
522         // TODO since periods are sorted, could optimize further
523 
524         switch (type1) {
525             case format:
526                 {
527                     if (fixedOrFlexible.span != null) {
528                         if (collisionIsErrorFormat(flexible, fixedOrFlexible.span, sampleError)) {
529                             return true;
530                         }
531                     } else { // flexible
532                         for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
533                             if (collisionIsErrorFormat(flexible, s, sampleError)) {
534                                 return true;
535                             }
536                         }
537                     }
538                     break;
539                 }
540             case selection:
541                 {
542                     if (fixedOrFlexible.span != null) {
543                         if (collisionIsErrorSelection(
544                                 flexible, fixedOrFlexible.span, sampleError)) {
545                             return true;
546                         }
547                     } else { // flexible
548                         for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
549                             if (collisionIsErrorSelection(flexible, s, sampleError)) {
550                                 return true;
551                             }
552                         }
553                     }
554                     break;
555                 }
556         }
557         return false; // no bad collision
558     }
559 
560     // Formatting has looser collision rules, because it is always paired with a time.
561     // That is, it is not a problem if two items collide,
562     // if it doesn't cause a collision when paired with a time.
563     // But if 11:00 has the same format (eg 11 X) as 23:00, there IS a collision.
564     // So we see if there is an overlap mod 12.
collisionIsErrorFormat( DayPeriod dayPeriod, Span other, Output<Integer> sampleError)565     private boolean collisionIsErrorFormat(
566             DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
567         int otherStart = other.start % NOON;
568         int otherEnd = other.getAdjustedEnd() % NOON;
569         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
570             int flexStart = s.start % NOON;
571             int flexEnd = s.getAdjustedEnd() % NOON;
572             if (otherStart <= flexEnd && otherEnd >= flexStart) { // overlap?
573                 if (sampleError != null) {
574                     sampleError.value = Math.max(otherStart, otherEnd);
575                 }
576                 return true;
577             }
578         }
579         return false;
580     }
581 
582     // Selection has stricter collision rules, because is is used to select different messages.
583     // So two types with the same localization do collide unless they have exactly the same rules.
collisionIsErrorSelection( DayPeriod dayPeriod, Span other, Output<Integer> sampleError)584     private boolean collisionIsErrorSelection(
585             DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
586         int otherStart = other.start;
587         int otherEnd = other.getAdjustedEnd();
588         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
589             int flexStart = s.start;
590             int flexEnd = s.getAdjustedEnd();
591             if (otherStart != flexStart) { // not same??
592                 if (sampleError != null) {
593                     sampleError.value = (otherStart + flexStart) / 2; // half-way between
594                 }
595                 return true;
596             } else if (otherEnd != flexEnd) { // not same??
597                 if (sampleError != null) {
598                     sampleError.value = (otherEnd + flexEnd) / 2; // half-way between
599                 }
600                 return true;
601             }
602         }
603         return false;
604     }
605 }
606