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 }