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