xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/DayPeriodInfo.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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         // we use the more lenient if they are mixed types
463         if (type2 == Type.format) {
464             type1 = Type.format;
465         }
466 
467         // At this point, they are unequal
468         // The fixed cannot overlap among themselves
469         final boolean fixed1 = dayPeriod1.isFixed();
470         final boolean fixed2 = dayPeriod2.isFixed();
471         if (fixed1 && fixed2) {
472             return true;
473         }
474         // at this point, at least one is flexible.
475         // make sure the second is not flexible.
476         DayPeriod fixedOrFlexible;
477         DayPeriod flexible;
478         if (fixed1) {
479             fixedOrFlexible = dayPeriod1;
480             flexible = dayPeriod2;
481         } else {
482             fixedOrFlexible = dayPeriod2;
483             flexible = dayPeriod1;
484         }
485 
486         // TODO since periods are sorted, could optimize further
487 
488         switch (type1) {
489             case format:
490                 {
491                     if (fixedOrFlexible.span != null) {
492                         if (collisionIsErrorFormat(flexible, fixedOrFlexible.span, sampleError)) {
493                             return true;
494                         }
495                     } else { // flexible
496                         for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
497                             if (collisionIsErrorFormat(flexible, s, sampleError)) {
498                                 return true;
499                             }
500                         }
501                     }
502                     break;
503                 }
504             case selection:
505                 {
506                     if (fixedOrFlexible.span != null) {
507                         if (collisionIsErrorSelection(
508                                 flexible, fixedOrFlexible.span, sampleError)) {
509                             return true;
510                         }
511                     } else { // flexible
512                         for (Span s : dayPeriodsToSpans.get(fixedOrFlexible)) {
513                             if (collisionIsErrorSelection(flexible, s, sampleError)) {
514                                 return true;
515                             }
516                         }
517                     }
518                     break;
519                 }
520         }
521         return false; // no bad collision
522     }
523 
524     // Formatting has looser collision rules, because it is always paired with a time.
525     // That is, it is not a problem if two items collide,
526     // if it doesn't cause a collision when paired with a time.
527     // But if 11:00 has the same format (eg 11 X) as 23:00, there IS a collision.
528     // So we see if there is an overlap mod 12.
collisionIsErrorFormat( DayPeriod dayPeriod, Span other, Output<Integer> sampleError)529     private boolean collisionIsErrorFormat(
530             DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
531         int otherStart = other.start % NOON;
532         int otherEnd = other.getAdjustedEnd() % NOON;
533         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
534             int flexStart = s.start % NOON;
535             int flexEnd = s.getAdjustedEnd() % NOON;
536             if (otherStart <= flexEnd && otherEnd >= flexStart) { // overlap?
537                 if (sampleError != null) {
538                     sampleError.value = Math.max(otherStart, otherEnd);
539                 }
540                 return true;
541             }
542         }
543         return false;
544     }
545 
546     // Selection has stricter collision rules, because is is used to select different messages.
547     // So two types with the same localization do collide unless they have exactly the same rules.
collisionIsErrorSelection( DayPeriod dayPeriod, Span other, Output<Integer> sampleError)548     private boolean collisionIsErrorSelection(
549             DayPeriod dayPeriod, Span other, Output<Integer> sampleError) {
550         int otherStart = other.start;
551         int otherEnd = other.getAdjustedEnd();
552         for (Span s : dayPeriodsToSpans.get(dayPeriod)) {
553             int flexStart = s.start;
554             int flexEnd = s.getAdjustedEnd();
555             if (otherStart != flexStart) { // not same??
556                 if (sampleError != null) {
557                     sampleError.value = (otherStart + flexStart) / 2; // half-way between
558                 }
559                 return true;
560             } else if (otherEnd != flexEnd) { // not same??
561                 if (sampleError != null) {
562                     sampleError.value = (otherEnd + flexEnd) / 2; // half-way between
563                 }
564                 return true;
565             }
566         }
567         return false;
568     }
569 }
570