1 package org.unicode.cldr.test; 2 3 import java.io.IOException; 4 import java.io.PrintWriter; 5 import java.text.ParseException; 6 import java.util.ArrayList; 7 import java.util.Calendar; 8 import java.util.Date; 9 import java.util.Iterator; 10 import java.util.List; 11 import java.util.Map; 12 import java.util.Set; 13 import java.util.TreeMap; 14 import java.util.TreeSet; 15 16 import org.unicode.cldr.draft.FileUtilities; 17 import org.unicode.cldr.util.CLDRFile; 18 import org.unicode.cldr.util.CLDRPaths; 19 import org.unicode.cldr.util.CldrUtility; 20 import org.unicode.cldr.util.Factory; 21 import org.unicode.cldr.util.Pair; 22 import org.unicode.cldr.util.SupplementalDataInfo; 23 import org.unicode.cldr.util.XPathParts; 24 25 import com.ibm.icu.impl.OlsonTimeZone; 26 import com.ibm.icu.impl.Relation; 27 import com.ibm.icu.text.DateFormat; 28 import com.ibm.icu.text.DecimalFormat; 29 import com.ibm.icu.text.NumberFormat; 30 import com.ibm.icu.text.SimpleDateFormat; 31 import com.ibm.icu.util.TimeZone; 32 import com.ibm.icu.util.TimeZoneTransition; 33 34 /** 35 * Verify that all zones in a metazone have the same behavior within the 36 * specified period. 37 * 38 * @author markdavis 39 * 40 */ 41 public class TestMetazones { 42 public static boolean DEBUG = false; 43 44 private static final long HOUR = 3600000; 45 private static final long DAY = 24 * 60 * 60 * 1000L; 46 private static final long MINUTE = 60000; 47 48 /** 49 * Set if we are suppressing daylight differences in the test. 50 */ 51 final static SupplementalDataInfo supplementalData = SupplementalDataInfo.getInstance(); 52 53 // WARNING: right now, the only metazone rules are in root, so that's all we're testing. 54 // if there were rules in other files, we'd have to check them to, by changing this line. 55 Factory factory = Factory.make(CLDRPaths.MAIN_DIRECTORY, "root"); 56 57 int errorCount = 0; 58 59 int warningCount = 0; 60 61 NumberFormat days = new DecimalFormat("0.000"); 62 NumberFormat hours = new DecimalFormat("+0.00;-0.00"); 63 PrintWriter log = null; 64 PrintWriter errorLog = null; 65 private boolean skipConsistency; 66 private boolean skipPartialDays; 67 private boolean noDaylight; 68 main(String[] args)69 public static void main(String[] args) throws IOException { 70 TimeZone.setDefault(TimeZone.getTimeZone("Etc/GMT")); 71 new TestMetazones().testAll(); 72 } 73 testAll()74 void testAll() throws IOException { 75 try { 76 noDaylight = CldrUtility.getProperty("nodaylight", null) != null; 77 skipPartialDays = CldrUtility.getProperty("skippartialdays", null, "") != null; 78 skipConsistency = CldrUtility.getProperty("skipconsistency", null, "") != null; 79 80 String exemplarOutFile = CldrUtility.getProperty("log", null, 81 CLDRPaths.GEN_DIRECTORY + "metazoneLog.txt"); 82 if (exemplarOutFile != null) { 83 log = FileUtilities.openUTF8Writer("", exemplarOutFile); 84 } 85 String errorOutFile = CldrUtility.getProperty("errors", null, 86 CLDRPaths.GEN_DIRECTORY + "metazoneErrors" + 87 (noDaylight ? "-noDaylight" : "") + 88 (skipPartialDays ? "-skipPartialDays" : "") 89 + ".txt"); 90 if (errorOutFile != null) { 91 errorLog = FileUtilities.openUTF8Writer("", errorOutFile); 92 } else { 93 errorLog = new PrintWriter(System.out); 94 } 95 96 for (String locale : factory.getAvailable()) { 97 test(locale); 98 } 99 } finally { 100 errorLog.println("Total Errors: " + errorCount); 101 errorLog.println("Total Warnings: " + warningCount); 102 if (log != null) { 103 log.close(); 104 } 105 if (errorLog != null) { 106 errorLog.close(); 107 } 108 } 109 } 110 111 /** 112 * Test a locale. 113 */ test(String locale)114 void test(String locale) { 115 CLDRFile file = factory.make(locale, false); 116 if (!fileHasMetazones(file)) { 117 return; 118 } 119 // testing zone information 120 errorLog.println("Testing metazone info in: " + locale); 121 // get the resolved version 122 file = factory.make(locale, true); 123 Relation<String, DateRangeAndZone> mzoneToData = Relation.<String, DateRangeAndZone> of( 124 new TreeMap<String, Set<DateRangeAndZone>>(), TreeSet.class); 125 126 Relation<String, DateRangeAndZone> zoneToDateRanges = Relation.<String, DateRangeAndZone> of( 127 new TreeMap<String, Set<DateRangeAndZone>>(), TreeSet.class); 128 129 fillMetazoneData(file, mzoneToData, zoneToDateRanges); 130 131 checkCoverage(zoneToDateRanges); 132 133 checkGapsAndOverlaps(zoneToDateRanges); 134 135 checkExemplars(mzoneToData, zoneToDateRanges); 136 if (skipConsistency) return; 137 138 checkMetazoneConsistency(mzoneToData); 139 } 140 fillMetazoneData(CLDRFile file, Relation<String, DateRangeAndZone> mzoneToData, Relation<String, DateRangeAndZone> zoneToDateRanges)141 private void fillMetazoneData(CLDRFile file, 142 Relation<String, DateRangeAndZone> mzoneToData, 143 Relation<String, DateRangeAndZone> zoneToDateRanges) { 144 for (String path : file) { 145 if (path.contains("/usesMetazone")) { 146 /* 147 * Sample: <zone type="Asia/Yerevan"> <usesMetazone to="1991-09-23" 148 * mzone="Yerevan"/> <usesMetazone from="1991-09-23" mzone="Armenia"/> 149 * </zone> 150 */ 151 XPathParts parts = XPathParts.getFrozenInstance(path); 152 String from = parts.getAttributeValue(-1, "from"); 153 long fromDate = DateRange.parse(from, false); 154 155 String to = parts.getAttributeValue(-1, "to"); 156 long toDate = DateRange.parse(to, true); 157 158 DateRange range = new DateRange(fromDate, toDate); 159 160 String mzone = parts.getAttributeValue(-1, "mzone"); 161 String zone = parts.getAttributeValue(-2, "type"); 162 163 mzoneToData.put(mzone, new DateRangeAndZone(zone, range)); 164 zoneToDateRanges.put(zone, new DateRangeAndZone(mzone, range)); 165 // errorLog.println(mzone + "\t" + new Data(zone, to, from)); 166 } 167 } 168 } 169 checkMetazoneConsistency( Relation<String, DateRangeAndZone> mzoneToData)170 private void checkMetazoneConsistency( 171 Relation<String, DateRangeAndZone> mzoneToData) { 172 errorLog.println(); 173 errorLog.println("*** Verify everything matches in metazones"); 174 errorLog.println(); 175 176 for (String mzone : mzoneToData.keySet()) { 177 if (DEBUG) { 178 errorLog.println(mzone); 179 } 180 Set<DateRangeAndZone> values = mzoneToData.getAll(mzone); 181 if (DEBUG) { 182 for (DateRangeAndZone value : values) { 183 errorLog.println("\t" + value); 184 } 185 } 186 for (DateRangeAndZone value : values) { 187 // quick and dirty test; make sure that everything matches over this 188 // interval 189 for (DateRangeAndZone value2 : values) { 190 // only do it once, so skip ones we've done the other direction 191 if (value2.compareTo(value) <= 0) { 192 continue; 193 } 194 // we have value and a different value2. Make sure that they have the 195 // same transition dates during any overlap 196 // errorLog.println("Comparing " + value + " to " + value2); 197 DateRange overlap = value.range.getOverlap(value2.range); 198 if (overlap.getExtent() == 0) { 199 continue; 200 } 201 202 OlsonTimeZone timezone1 = new OlsonTimeZone(value.zone); 203 OlsonTimeZone timezone2 = new OlsonTimeZone(value2.zone); 204 List<Pair<Long, Long>> list = getDifferencesOverRange(timezone1, timezone2, overlap); 205 206 if (list.size() != 0) { 207 errln("Zones " + showZone(value.zone) + " and " + showZone(value2.zone) 208 + " shouldn't be in the same metazone <" + mzone + "> during the period " 209 + overlap + ". " + "Sample dates:" + CldrUtility.LINE_SEPARATOR + "\t" 210 + showDifferences(timezone1, timezone2, list)); 211 } 212 } 213 } 214 } 215 } 216 showZone(String zone)217 private String showZone(String zone) { 218 // TODO Auto-generated method stub 219 return zone + " [" + supplementalData.getZone_territory(zone) + "]"; 220 } 221 showDifferences(OlsonTimeZone zone1, OlsonTimeZone zone2, List<Pair<Long, Long>> list)222 String showDifferences(OlsonTimeZone zone1, OlsonTimeZone zone2, 223 List<Pair<Long, Long>> list) { 224 225 StringBuffer buffer = new StringBuffer(); 226 227 int count = 0; 228 boolean abbreviating = list.size() > 7; 229 long totalErrorPeriod = 0; 230 for (Pair<Long, Long> pair : list) { 231 count++; 232 long start = pair.getFirst(); 233 long end = pair.getSecond(); 234 int startDelta = getOffset(zone1, start) - getOffset(zone2, start); 235 int endDelta = getOffset(zone1, end) - getOffset(zone2, end); 236 if (startDelta != endDelta) { 237 showDeltas(zone1, zone2, start, end); 238 throw new IllegalArgumentException(); 239 } 240 final long errorPeriod = end - start + MINUTE; 241 totalErrorPeriod += errorPeriod; 242 if (abbreviating) { 243 if (count == 4) 244 buffer.append("..." + CldrUtility.LINE_SEPARATOR + "\t"); 245 if (count >= 4 && count < list.size() - 2) 246 continue; 247 } 248 249 buffer.append("delta=\t" 250 + hours.format(startDelta / (double) HOUR) + " hours:\t" + DateRange.format(start) + "\tto\t" + 251 DateRange.format(end) + ";\ttotal:\t" + days.format((errorPeriod) / (double) DAY) + " days" 252 + CldrUtility.LINE_SEPARATOR + "\t"); 253 } 254 buffer.append("\tTotal Period in Error:\t" + days.format((totalErrorPeriod) / (double) DAY) + " days"); 255 return buffer.toString(); 256 } 257 showDeltas(OlsonTimeZone zone1, OlsonTimeZone zone2, long start, long end)258 private void showDeltas(OlsonTimeZone zone1, OlsonTimeZone zone2, long start, long end) { 259 errorLog.println(zone1.getID() + ", start: " + start + ", startOffset " + getOffset(zone1, start)); 260 errorLog.println(zone1.getID() + ", end: " + start + ", endOffset " + getOffset(zone1, end)); 261 errorLog.println(zone2.getID() + ", start: " + start + ", startOffset " + getOffset(zone2, start)); 262 errorLog.println(zone2.getID() + ", end: " + start + ", endOffset " + getOffset(zone2, end)); 263 } 264 265 /** 266 * Returns a list of pairs. The delta timezone offsets for both zones should be identical between each of the points 267 * in the pair 268 * 269 * @param zone1 270 * @param zone2 271 * @param overlap 272 * @return 273 */ getDifferencesOverRange(OlsonTimeZone zone1, OlsonTimeZone zone2, DateRange overlap)274 private List<Pair<Long, Long>> getDifferencesOverRange(OlsonTimeZone zone1, OlsonTimeZone zone2, DateRange overlap) { 275 Set<Long> list1 = new TreeSet<>(); 276 addTransitions(zone1, overlap, list1); 277 addTransitions(zone2, overlap, list1); 278 279 // Remove any transition points that keep the same delta relationship 280 List<Long> list = new ArrayList<>(); 281 int lastDelta = 0; 282 for (long point : list1) { 283 int offset1 = getOffset(zone1, point); 284 int offset2 = getOffset(zone2, point); 285 int delta = offset1 - offset2; 286 if (delta != lastDelta) { 287 list.add(point); 288 lastDelta = delta; 289 } 290 } 291 292 // now combine into a list of start/end pairs 293 List<Pair<Long, Long>> result = new ArrayList<>(); 294 long lastPoint = Long.MIN_VALUE; 295 for (long point : list) { 296 if (lastPoint != Long.MIN_VALUE) { 297 long start = lastPoint; 298 long end = point - MINUTE; 299 if (DEBUG && start == 25678800000L && end == 33193740000L) { 300 errorLog.println("debugStop"); 301 showDeltas(zone1, zone2, start, end); 302 } 303 304 int startOffset1 = getOffset(zone1, start); 305 int startOffset2 = getOffset(zone2, start); 306 307 int endOffset1 = getOffset(zone1, end); 308 int endOffset2 = getOffset(zone2, end); 309 310 final int startDelta = startOffset1 - startOffset2; 311 final int endDelta = endOffset1 - endOffset2; 312 313 if (startDelta != endDelta) { 314 throw new IllegalArgumentException("internal error"); 315 } 316 317 if (startDelta != 0) { 318 if (skipPartialDays && end - start < DAY) { 319 // do nothing 320 } else { 321 result.add(new Pair<>(start, end)); // back up 1 minute 322 } 323 } 324 } 325 lastPoint = point; 326 } 327 return result; 328 } 329 330 /** 331 * My own private version so I can suppress daylight. 332 * 333 * @param zone1 334 * @param point 335 * @return 336 */ getOffset(OlsonTimeZone zone1, long point)337 private int getOffset(OlsonTimeZone zone1, long point) { 338 int offset1 = zone1.getOffset(point); 339 if (noDaylight && zone1.inDaylightTime(new Date(point))) offset1 -= 3600000; 340 return offset1; 341 } 342 addTransitions(OlsonTimeZone zone1, DateRange overlap, Set<Long> list)343 private void addTransitions(OlsonTimeZone zone1, DateRange overlap, Set<Long> list) { 344 long startTime = overlap.startDate; 345 long endTime = overlap.endDate; 346 list.add(startTime); 347 list.add(endTime); 348 while (true) { 349 TimeZoneTransition transition = zone1.getNextTransition(startTime, false); 350 if (transition == null) 351 break; 352 long newTime = transition.getTime(); 353 if (newTime > endTime) { 354 break; 355 } 356 list.add(newTime); 357 startTime = newTime; 358 } 359 } 360 checkGapsAndOverlaps( Relation<String, DateRangeAndZone> zoneToDateRanges)361 private void checkGapsAndOverlaps( 362 Relation<String, DateRangeAndZone> zoneToDateRanges) { 363 errorLog.println(); 364 errorLog.println("*** Verify no gaps or overlaps in zones"); 365 for (String zone : zoneToDateRanges.keySet()) { 366 if (DEBUG) { 367 errorLog.println(zone); 368 } 369 Set<DateRangeAndZone> values = zoneToDateRanges.getAll(zone); 370 long last = DateRange.MIN_DATE; 371 for (DateRangeAndZone value : values) { 372 if (DEBUG) { 373 errorLog.println("\t" + value); 374 } 375 checkGapOrOverlap(last, value.range.startDate); 376 last = value.range.endDate; 377 } 378 checkGapOrOverlap(last, DateRange.MAX_DATE); 379 } 380 } 381 checkExemplars( Relation<String, DateRangeAndZone> mzoneToData, Relation<String, DateRangeAndZone> zoneToData)382 private void checkExemplars( 383 Relation<String, DateRangeAndZone> mzoneToData, 384 Relation<String, DateRangeAndZone> zoneToData) { 385 386 if (log != null) { 387 log.println(); 388 log.println("Mapping from Zones to Metazones"); 389 log.println(); 390 for (String zone : zoneToData.keySet()) { 391 log.println(zone); 392 for (DateRangeAndZone value : zoneToData.getAll(zone)) { 393 log.println("\t" + value.zone + "\t" + value.range); 394 } 395 } 396 log.println(); 397 log.println("Mapping from Metazones to Zones"); 398 log.println(); 399 } 400 401 errorLog.println(); 402 errorLog 403 .println("*** Verify that every metazone has at least one zone that is always in that metazone, over the span of the metazone's existance."); 404 errorLog.println(); 405 406 // get the best exemplars 407 408 Map<String, Map<String, String>> metazoneToRegionToZone = supplementalData.getMetazoneToRegionToZone(); 409 410 for (String mzone : mzoneToData.keySet()) { 411 if (DEBUG) { 412 errorLog.println(mzone); 413 } 414 415 // get the best zone 416 final String bestZone = metazoneToRegionToZone.get(mzone).get("001"); 417 if (bestZone == null) { 418 errorLog.println("Metazone <" + mzone + "> is missing a 'best zone' (for 001) in supplemental data."); 419 } 420 Set<DateRangeAndZone> values = mzoneToData.getAll(mzone); 421 422 Map<String, DateRanges> zoneToRanges = new TreeMap<>(); 423 DateRanges mzoneRanges = new DateRanges(); 424 // first determine what the max and min dates are 425 426 for (DateRangeAndZone value : values) { 427 DateRanges ranges = zoneToRanges.get(value.zone); 428 if (ranges == null) { 429 zoneToRanges.put(value.zone, ranges = new DateRanges()); 430 } 431 ranges.add(value.range); 432 mzoneRanges.add(value.range); 433 } 434 435 if (bestZone != null && !zoneToRanges.keySet().contains(bestZone)) { 436 zoneToRanges.keySet().contains(bestZone); 437 errorLog.println("The 'best zone' (" + showZone(bestZone) + ") for the metazone <" + mzone 438 + "> is not in the metazone!"); 439 } 440 441 // now see how many there are 442 int count = 0; 443 if (log != null) { 444 log.println(mzone + ":\t" + mzoneRanges); 445 } 446 for (String zone : zoneToRanges.keySet()) { 447 final boolean isComplete = mzoneRanges.equals(zoneToRanges.get(zone)); 448 if (zone.equals(bestZone) && !isComplete) { 449 errorLog.println("The 'best zone' (" + showZone(bestZone) + ") for the metazone <" + mzone 450 + "> is only partially in the metazone!"); 451 } 452 if (isComplete) { 453 count++; 454 } 455 if (log != null) { 456 log.println("\t" + zone + ":\t" 457 + supplementalData.getZone_territory(zone) + "\t" 458 + zoneToRanges.get(zone) + (isComplete ? "" : "\t\tPartial")); 459 } 460 461 } 462 463 // show the errors 464 if (count == 0) { 465 errln("Metazone <" + mzone + "> does not have exemplar for whole span: " + mzoneRanges); 466 for (DateRangeAndZone value : values) { 467 errorLog.println("\t" + mzone + ":\t" + value); 468 for (DateRangeAndZone mvalues : zoneToData.getAll(value.zone)) { 469 errorLog.println("\t\t\t" + showZone(value.zone) + ":\t" + mvalues); 470 } 471 } 472 errorLog.println("====="); 473 for (String zone : zoneToRanges.keySet()) { 474 errorLog.println("\t\t\t" + zone + ":\t" + zoneToRanges.get(zone)); 475 } 476 } 477 } 478 } 479 checkCoverage(Relation<String, DateRangeAndZone> zoneToDateRanges)480 private void checkCoverage(Relation<String, DateRangeAndZone> zoneToDateRanges) { 481 errorLog.println(); 482 errorLog.println("*** Verify coverage of canonical zones"); 483 errorLog.println(); 484 Set<String> canonicalZones = supplementalData.getCanonicalZones(); 485 Set<String> missing = new TreeSet<>(canonicalZones); 486 missing.removeAll(zoneToDateRanges.keySet()); 487 for (Iterator<String> it = missing.iterator(); it.hasNext();) { 488 String value = it.next(); 489 if (value.startsWith("Etc/")) { 490 it.remove(); 491 } 492 } 493 if (missing.size() != 0) { 494 errln("Missing canonical zones: " + missing); 495 } 496 Set<String> extras = new TreeSet<>(zoneToDateRanges.keySet()); 497 extras.removeAll(canonicalZones); 498 if (extras.size() != 0) { 499 errln("Superfluous zones (not canonical): " + extras); 500 } 501 } 502 checkGapOrOverlap(long last, long nextDate)503 private void checkGapOrOverlap(long last, long nextDate) { 504 if (last != nextDate) { 505 if (last < nextDate) { 506 warnln("Gap in coverage: " + DateRange.format(last) + ", " 507 + DateRange.format(nextDate)); 508 } else { 509 errln("Overlap in coverage: " + DateRange.format(last) + ", " 510 + DateRange.format(nextDate)); 511 } 512 } 513 } 514 errln(String string)515 private void errln(String string) { 516 errorLog.println("ERROR: " + string); 517 errorCount++; 518 } 519 warnln(String string)520 private void warnln(String string) { 521 errorLog.println("WARNING: " + string); 522 warningCount++; 523 } 524 525 /** 526 * Stores a range and a zone. The zone might be a timezone or metazone. 527 * 528 * @author markdavis 529 * 530 */ 531 static class DateRangeAndZone implements Comparable<DateRangeAndZone> { 532 DateRange range; 533 534 String zone; 535 DateRangeAndZone(String zone, String startDate, String endDate)536 public DateRangeAndZone(String zone, String startDate, String endDate) { 537 this(zone, new DateRange(startDate, endDate)); 538 } 539 DateRangeAndZone(String zone, DateRange range)540 public DateRangeAndZone(String zone, DateRange range) { 541 this.range = range; 542 this.zone = zone; 543 } 544 545 @Override compareTo(DateRangeAndZone other)546 public int compareTo(DateRangeAndZone other) { 547 int result = range.compareTo(other.range); 548 if (result != 0) 549 return result; 550 return zone.compareTo(other.zone); 551 } 552 553 @Override toString()554 public String toString() { 555 return "{" + range + " => " + zone + "}"; 556 } 557 } 558 559 static class DateRanges { 560 Set<DateRange> contents = new TreeSet<>(); 561 add(DateRange o)562 public void add(DateRange o) { 563 contents.add(o); 564 // now fix overlaps. Dumb implementation for now 565 // they are ordered by start date, so just check that adjacent ones don't touch 566 while (true) { 567 boolean madeFix = false; 568 DateRange last = null; 569 for (DateRange range : contents) { 570 if (last != null && last.containsSome(range)) { 571 madeFix = true; 572 DateRange newRange = last.getUnion(range); 573 contents.remove(last); 574 contents.remove(range); 575 contents.add(newRange); 576 } 577 last = range; 578 } 579 if (!madeFix) break; 580 } 581 } 582 contains(DateRanges other)583 boolean contains(DateRanges other) { 584 for (DateRange otherRange : other.contents) { 585 if (!contains(otherRange)) { 586 return false; 587 } 588 } 589 return true; 590 } 591 contains(DateRange otherRange)592 private boolean contains(DateRange otherRange) { 593 for (DateRange range : contents) { 594 if (!range.containsAll(otherRange)) { 595 return false; 596 } 597 } 598 return true; 599 } 600 601 @Override equals(Object other)602 public boolean equals(Object other) { 603 return contents.equals(((DateRanges) other).contents); 604 } 605 606 @Override hashCode()607 public int hashCode() { 608 return contents.hashCode(); 609 } 610 611 @Override toString()612 public String toString() { 613 return contents.toString(); 614 } 615 } 616 617 static class DateRange implements Comparable<DateRange> { 618 long startDate; 619 620 long endDate; 621 DateRange(String startDate, String endDate)622 public DateRange(String startDate, String endDate) { 623 this(parse(startDate, false), parse(endDate, true)); 624 } 625 containsAll(DateRange otherRange)626 public boolean containsAll(DateRange otherRange) { 627 return startDate <= otherRange.startDate && otherRange.endDate <= endDate; 628 } 629 630 /** 631 * includes cases where they touch. 632 * 633 * @param otherRange 634 * @return 635 */ containsNone(DateRange otherRange)636 public boolean containsNone(DateRange otherRange) { 637 return startDate > otherRange.endDate || otherRange.startDate > endDate; 638 } 639 640 /** 641 * includes cases where they touch. 642 * 643 * @param otherRange 644 * @return 645 */ containsSome(DateRange otherRange)646 public boolean containsSome(DateRange otherRange) { 647 return startDate <= otherRange.endDate && otherRange.startDate <= endDate; 648 } 649 DateRange(long startDate, long endDate)650 public DateRange(long startDate, long endDate) { 651 this.startDate = startDate; 652 this.endDate = endDate; 653 } 654 getExtent()655 public long getExtent() { 656 return endDate - startDate; 657 } 658 getOverlap(DateRange other)659 public DateRange getOverlap(DateRange other) { 660 long start = startDate; 661 if (start < other.startDate) { 662 start = other.startDate; 663 } 664 long end = endDate; 665 if (end > other.endDate) { 666 end = other.endDate; 667 } 668 // make sure we are ordered 669 if (end < start) { 670 end = start; 671 } 672 return new DateRange(start, end); 673 } 674 getUnion(DateRange other)675 public DateRange getUnion(DateRange other) { 676 long start = startDate; 677 if (start > other.startDate) { 678 start = other.startDate; 679 } 680 long end = endDate; 681 if (end < other.endDate) { 682 end = other.endDate; 683 } 684 // make sure we are ordered 685 if (end < start) { 686 end = start; 687 } 688 return new DateRange(start, end); 689 } 690 parse(String date, boolean end)691 static long parse(String date, boolean end) { 692 if (date == null) 693 return end ? MAX_DATE : MIN_DATE; 694 try { 695 return iso1.parse(date).getTime(); 696 } catch (ParseException e) { 697 try { 698 return iso2.parse(date).getTime(); 699 } catch (ParseException e2) { 700 throw new IllegalArgumentException("unexpected error in data", e); 701 } 702 } 703 } 704 705 static DateFormat iso1 = new SimpleDateFormat("yyyy-MM-dd HH:mm"); 706 707 static DateFormat iso2 = new SimpleDateFormat("yyyy-MM-dd"); 708 709 @Override compareTo(DateRange other)710 public int compareTo(DateRange other) { 711 if (startDate < other.startDate) 712 return -1; 713 if (startDate > other.startDate) 714 return 1; 715 if (endDate < other.endDate) 716 return -1; 717 if (endDate > other.endDate) 718 return 1; 719 return 0; 720 } 721 722 // Get Date-Time in milliseconds getDateTimeinMillis(int year, int month, int date, int hourOfDay, int minute, int second)723 private static long getDateTimeinMillis(int year, int month, int date, int hourOfDay, int minute, int second) { 724 Calendar cal = Calendar.getInstance(); 725 cal.set(year, month, date, hourOfDay, minute, second); 726 return cal.getTimeInMillis(); 727 } 728 729 static long MIN_DATE = getDateTimeinMillis(70, 0, 1, 0, 0, 0); 730 731 static long MAX_DATE = getDateTimeinMillis(110, 0, 1, 0, 0, 0); 732 733 @Override toString()734 public String toString() { 735 return "{" + format(startDate) + " to " + format(endDate) + "}"; 736 } 737 format(Date date)738 public static String format(Date date) { 739 return (// date.equals(MIN_DATE) ? "-∞" : date.equals(MAX_DATE) ? "+∞" : 740 iso1.format(date)); 741 } 742 format(long date)743 public static String format(long date) { 744 return format(new Date(date)); 745 } 746 747 } 748 fileHasMetazones(CLDRFile file)749 boolean fileHasMetazones(CLDRFile file) { 750 for (String path : file) { 751 if (path.contains("usesMetazone")) 752 return true; 753 } 754 return false; 755 } 756 }