Home | History | Annotate | Download | only in test
      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 
     71     public static void main(String[] args) throws IOException {
     72         TimeZone.setDefault(TimeZone.getTimeZone("Etc/GMT"));
     73         new TestMetazones().testAll();
     74     }
     75 
     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      */
    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 
    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 
    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 
    219     private String showZone(String zone) {
    220         // TODO Auto-generated method stub
    221         return zone + " [" + supplementalData.getZone_territory(zone) + "]";
    222     }
    223 
    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 
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    518     private void errln(String string) {
    519         errorLog.println("ERROR: " + string);
    520         errorCount++;
    521     }
    522 
    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 
    539         public DateRangeAndZone(String zone, String startDate, String endDate) {
    540             this(zone, new DateRange(startDate, endDate));
    541         }
    542 
    543         public DateRangeAndZone(String zone, DateRange range) {
    544             this.range = range;
    545             this.zone = zone;
    546         }
    547 
    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 
    555         public String toString() {
    556             return "{" + range + " => " + zone + "}";
    557         }
    558     }
    559 
    560     static class DateRanges {
    561         Set<DateRange> contents = new TreeSet<DateRange>();
    562 
    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 
    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 
    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 
    602         public boolean equals(Object other) {
    603             return contents.equals(((DateRanges) other).contents);
    604         }
    605 
    606         public int hashCode() {
    607             return contents.hashCode();
    608         }
    609 
    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 
    620         public DateRange(String startDate, String endDate) {
    621             this(parse(startDate, false), parse(endDate, true));
    622         }
    623 
    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          */
    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          */
    644         public boolean containsSome(DateRange otherRange) {
    645             return startDate <= otherRange.endDate && otherRange.startDate <= endDate;
    646         }
    647 
    648         public DateRange(long startDate, long endDate) {
    649             this.startDate = startDate;
    650             this.endDate = endDate;
    651         }
    652 
    653         public long getExtent() {
    654             return endDate - startDate;
    655         }
    656 
    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 
    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 
    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 
    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
    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 
    730         public String toString() {
    731             return "{" + format(startDate) + " to " + format(endDate) + "}";
    732         }
    733 
    734         public static String format(Date date) {
    735             return (// date.equals(MIN_DATE) ? "-" : date.equals(MAX_DATE) ? "+" :
    736             iso1.format(date));
    737         }
    738 
    739         public static String format(long date) {
    740             return format(new Date(date));
    741         }
    742 
    743     }
    744 
    745     boolean fileHasMetazones(CLDRFile file) {
    746         for (String path : file) {
    747             if (path.contains("usesMetazone"))
    748                 return true;
    749         }
    750         return false;
    751     }
    752 }