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