• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }