2 *******************************************************************************
\r
3 * Copyright (C) 2002-2010, International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 *******************************************************************************
\r
7 package com.ibm.icu.dev.test.timezone;
\r
9 import java.text.DateFormat;
\r
10 import java.text.NumberFormat;
\r
11 import java.util.ArrayList;
\r
12 import java.util.Calendar;
\r
13 import java.util.Collection;
\r
14 import java.util.Date;
\r
15 import java.util.GregorianCalendar;
\r
16 import java.util.HashMap;
\r
17 import java.util.Iterator;
\r
18 import java.util.List;
\r
19 import java.util.Locale;
\r
20 import java.util.Map;
\r
21 import java.util.Set;
\r
22 import java.util.TreeSet;
\r
24 import com.ibm.icu.dev.test.TestFmwk;
\r
25 import com.ibm.icu.util.TimeZone;
\r
29 * Class for testing TimeZones for consistency
\r
33 public class TimeZoneAliasTest extends TestFmwk {
\r
35 public static void main(String[] args) throws Exception {
\r
36 new TimeZoneAliasTest().run(args);
\r
40 * There are two things to check aliases for:<br>
\r
41 * 1. the alias set must be uniform: if a isAlias b, then aliasSet(a) == aliasSet(b)<br>
\r
42 * 2. all aliases must have the same offsets
\r
44 public void TestAliases() {
\r
45 Zone.Seconds seconds = new Zone.Seconds();
\r
46 for (Iterator it = Zone.getZoneSet().iterator(); it.hasNext(); ) {
\r
47 Zone zone = (Zone)it.next();
\r
48 String id = zone.id;
\r
49 if (id.indexOf('/') < 0 && (id.endsWith("ST") || id.endsWith("DT"))) {
\r
50 if (zone.minRecentOffset != zone.maxRecentOffset) {
\r
52 "Standard or Daylight Time not constant: " + id
\r
53 + ": " + Zone.formatHours(zone.minRecentOffset)
\r
54 + " != " + Zone.formatHours(zone.maxRecentOffset));
\r
57 Set aliases = zone.getPurportedAliases();
\r
58 Set aliasesSet = new TreeSet(aliases);
\r
59 aliasesSet.add(id); // for comparison
\r
60 Iterator aliasIterator = aliases.iterator();
\r
61 while (aliasIterator.hasNext()) {
\r
62 String otherId = (String)aliasIterator.next();
\r
63 Zone otherZone = Zone.make(otherId);
\r
64 Set otherAliases = otherZone.getPurportedAliases();
\r
65 otherAliases.add(otherId); // for comparison
\r
66 if (!aliasesSet.equals(otherAliases)) {
\r
68 "Aliases Unsymmetric: "
\r
69 + id + " => " + Zone.bf.join(aliasesSet)
\r
71 + otherId + " => " + Zone.bf.join(otherAliases));
\r
73 if (zone.findOffsetOrdering(otherZone, seconds) != 0) {
\r
74 errln("Aliases differ: " + id + ", " + otherId
\r
75 + " differ at " + seconds);
\r
82 * We check to see that every timezone that is not an alias is actually different!
\r
84 public void TestDifferences() {
\r
86 Zone.Seconds diffDate = new Zone.Seconds();
\r
87 for (Iterator it = Zone.getZoneSet().iterator(); it.hasNext();) {
\r
88 Zone testZone = (Zone)it.next();
\r
90 String common = testZone + "\tvs " + last + ":\t";
\r
91 int diff = testZone.findOffsetOrdering(last, diffDate);
\r
93 logln("\t" + common + "difference at: " + diffDate
\r
94 + ", " + Zone.formatHours(diff) + "hr");
\r
95 } else if (testZone.isRealAlias(last)) {
\r
96 logln("\t" + common + "alias, no difference");
\r
98 errln(common + "NOT ALIAS BUT NO DIFFERENCE!");
\r
106 * Utility for printing out zones to be translated.
\r
108 public static void TestGenerateZones() {
\r
110 for (Iterator it = Zone.getUniqueZoneSet().iterator(); it.hasNext();) {
\r
111 Zone zone = (Zone)it.next();
\r
112 System.out.println(zone.toString(count++));
\r
116 /** Utility; ought to be someplace common
\r
118 // remove dependency on bagformatter for now
\r
119 static class CollectionJoiner {
\r
120 private String separator;
\r
121 CollectionJoiner(String separator) {
\r
122 this.separator = separator;
\r
124 String join(Collection c) {
\r
125 StringBuffer result = new StringBuffer();
\r
126 boolean isFirst = true;
\r
127 for (Iterator it = c.iterator(); it.hasNext(); ) {
\r
128 if (!isFirst) result.append(separator);
\r
129 else isFirst = false;
\r
130 result.append(it.next().toString());
\r
132 return result.toString();
\r
138 * The guts is in this subclass. It sucks in all the data from the zones,
\r
139 * and analyses it. It constructs some mappings for the unique ids,
\r
141 * The main tricky bit is that for performance it pre-analyses all zones
\r
142 * for inflections points; the points in time where the offset changes.
\r
143 * The zones can then be sorted by those points, which allows us to
\r
144 * avoid expensive comparisons.
\r
147 static class Zone implements Comparable {
\r
149 // static private final BagFormatter bf = new BagFormatter().setSeparator(", ");
\r
150 private static final CollectionJoiner bf = new CollectionJoiner(", ");
\r
151 static private final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
\r
152 static private final NumberFormat nf = NumberFormat.getInstance(Locale.US);
\r
153 static private final long HOUR = 1000*60*60;
\r
154 static private final double DHOUR = HOUR;
\r
155 static private final long DAY = 24*HOUR;
\r
156 static private final long GROSS_PERIOD = 30*DAY;
\r
157 static private final long EPSILON = HOUR/4;
\r
158 static private final int currentYear = new GregorianCalendar().get(Calendar.YEAR);
\r
159 static private final long endDate = getDate((currentYear+1),0,1).getTime();
\r
160 static private final long endDate2 = getDate((currentYear+1),6,1).getTime();
\r
161 static private final long recentLimit = getDate((currentYear-1),6,1).getTime();
\r
162 static private final long startDate = getDate(1905,0,1).getTime();
\r
164 static private final Map idToZone = new HashMap();
\r
165 static private final Set zoneSet = new TreeSet();
\r
166 static private final Set uniqueZoneSet = new TreeSet();
\r
167 static private final Map idToRealAliases = new HashMap();
\r
169 // build everything once.
\r
171 String [] foo = TimeZone.getAvailableIDs();
\r
172 for (int i = 0; i < foo.length; ++i) {
\r
173 zoneSet.add(Zone.make(foo[i]));
\r
176 Zone.Seconds diffDate = new Zone.Seconds();
\r
177 String lastUnique = "";
\r
178 for (Iterator it = Zone.getZoneSet().iterator(); it.hasNext();) {
\r
179 Zone testZone = (Zone)it.next();
\r
180 if (last == null) {
\r
181 uniqueZoneSet.add(testZone);
\r
182 lastUnique = testZone.id;
\r
184 int diff = testZone.findOffsetOrdering(last, diffDate);
\r
186 uniqueZoneSet.add(testZone);
\r
187 lastUnique = testZone.id;
\r
189 Set aliases = (Set)idToRealAliases.get(lastUnique);
\r
190 if (aliases == null) {
\r
191 aliases = new TreeSet();
\r
192 idToRealAliases.put(lastUnique, aliases);
\r
194 aliases.add(testZone.id);
\r
201 static public Set getZoneSet() {
\r
205 public static Set getUniqueZoneSet() {
\r
206 return uniqueZoneSet;
\r
209 static public Zone make(String id) {
\r
210 Zone result = (Zone)idToZone.get(id);
\r
211 if (result != null) return result;
\r
212 result = new Zone(id);
\r
213 idToZone.put(id, result);
\r
217 static public String formatHours(int hours) {
\r
218 return nf.format(hours/DHOUR);
\r
221 // utility class for date return, because Date is clunky.
\r
222 public static class Seconds {
\r
223 public long seconds = Long.MIN_VALUE;
\r
224 public String toString() {
\r
225 if (seconds == Long.MIN_VALUE) return "n/a";
\r
226 return df.format(new Date(seconds));
\r
231 // we keep min/max offsets not only over all time (that we care about)
\r
232 // but also separate ones for recent years.
\r
234 private TimeZone zone;
\r
236 private int minOffset;
\r
237 private int maxOffset;
\r
238 private int minRecentOffset;
\r
239 private int maxRecentOffset;
\r
240 private List inflectionPoints = new ArrayList();
\r
241 private Set purportedAliases = new TreeSet();
\r
243 private Zone(String id) { // for interal use only; use make instead!
\r
244 zone = TimeZone.getTimeZone(id);
\r
248 int equivCount = TimeZone.countEquivalentIDs(id);
\r
249 for (int j = 0; j < equivCount; ++j) {
\r
250 String altID = TimeZone.getEquivalentID(id, j);
\r
251 if (altID.equals(id)) continue;
\r
252 purportedAliases.add(altID);
\r
255 // find inflexion points; times where the offset changed
\r
256 long lastDate = endDate;
\r
257 if (zone.getOffset(lastDate) < zone.getOffset(endDate2)) lastDate = endDate2;
\r
258 maxRecentOffset = minRecentOffset = minOffset = maxOffset = zone.getOffset(lastDate);
\r
260 inflectionPoints.add(new Long(lastDate));
\r
261 int lastOffset = zone.getOffset(endDate);
\r
262 long lastInflection = endDate;
\r
264 // we do a gross search, then narrow in when we find a difference from the last one
\r
265 for (long currentDate = endDate; currentDate >= startDate; currentDate -= GROSS_PERIOD) {
\r
266 int currentOffset = zone.getOffset(currentDate);
\r
267 if (currentOffset != lastOffset) { // Binary Search
\r
268 if (currentOffset < minOffset) minOffset = currentOffset;
\r
269 if (currentOffset > maxOffset) maxOffset = currentOffset;
\r
270 if (lastInflection >= recentLimit) {
\r
271 if (currentOffset < minRecentOffset) minRecentOffset = currentOffset;
\r
272 if (currentOffset > maxRecentOffset) maxRecentOffset = currentOffset;
\r
274 long low = currentDate;
\r
275 long high = lastDate;
\r
276 while (low - high > EPSILON) {
\r
277 long mid = (high + low)/2;
\r
278 int midOffset = zone.getOffset(mid);
\r
279 if (midOffset == low) {
\r
285 inflectionPoints.add(new Long(low));
\r
286 lastInflection = low;
\r
288 lastOffset = currentOffset;
\r
290 inflectionPoints.add(new Long(startDate)); // just to cap it off for comparisons.
\r
293 // we assume that places will not convert time zones then back within one day
\r
294 // so we go first by half
\r
295 public int findOffsetOrdering(Zone other, Seconds dateDiffFound) {
\r
296 //System.out.println("-diff: " + id + "\t" + other.id);
\r
299 int min = inflectionPoints.size();
\r
300 if (other.inflectionPoints.size() < min) min = other.inflectionPoints.size();
\r
303 for (int i = 0; i < min; ++i) {
\r
304 long myIP = ((Long)inflectionPoints.get(i)).longValue();
\r
305 long otherIP = ((Long)other.inflectionPoints.get(i)).longValue();
\r
306 if (myIP > otherIP) { // take lowest, for transitivity (semi)
\r
311 result = zone.getOffset(myIP) - other.zone.getOffset(myIP);
\r
316 if (myIP == otherIP) continue; // test other if different
\r
318 result = zone.getOffset(myIP) - other.zone.getOffset(myIP);
\r
324 // if they are equal so far, we don't care about the rest
\r
326 seconds = Long.MIN_VALUE;
\r
329 //System.out.println("+diff: " + (result/HOUR) + "\t" + dateDiffFound);
\r
330 if (dateDiffFound != null) dateDiffFound.seconds = seconds;
\r
334 // internal buffer to avoid creation all the time.
\r
335 private Seconds diffDateReturn = new Seconds();
\r
337 public int compareTo(Object o) {
\r
338 Zone other = (Zone)o;
\r
339 // first order by max and min offsets
\r
340 // min will usually correspond to standard time, max to daylight
\r
341 // unless there have been historical shifts
\r
342 if (minRecentOffset < other.minRecentOffset) return -1;
\r
343 if (minRecentOffset > other.minRecentOffset) return 1;
\r
344 if (maxRecentOffset < other.maxRecentOffset) return -1;
\r
345 if (maxRecentOffset > other.maxRecentOffset) return 1;
\r
346 // now check that all offsets are the same over history
\r
347 int diffDate = findOffsetOrdering(other, diffDateReturn);
\r
348 if (diffDate != 0) return diffDate;
\r
349 // choose longer name first!!
\r
350 if (id.length() != other.id.length()) {
\r
351 if (id.length() < other.id.length()) return 1;
\r
354 return id.compareTo(other.id);
\r
357 public Set getPurportedAliases() {
\r
358 return new TreeSet(purportedAliases); // clone for safety
\r
361 public boolean isPurportedAlias(String zoneID) {
\r
362 return purportedAliases.contains(zoneID);
\r
365 public boolean isRealAlias(Zone z) {
\r
366 return purportedAliases.contains(z.id);
\r
369 public String getPurportedAliasesAsString() {
\r
370 Set s = getPurportedAliases();
\r
371 if (s.size() == 0) return "";
\r
372 return " " + bf.join(s);
\r
375 public String getRealAliasesAsString() {
\r
376 Set s = (Set)idToRealAliases.get(id);
\r
377 if (s == null) return "";
\r
378 return " *" + bf.join(s);
\r
381 public String getCity() {
\r
382 int pos = id.lastIndexOf(('/'));
\r
383 String city = id.substring(pos+1);
\r
384 return city.replace('_',' ');
\r
387 public String toString() {
\r
388 return toString(-1);
\r
392 * Where count > 0, returns string that is set up for translation
\r
394 public String toString(int count) {
\r
395 String city = getCity();
\r
396 String hours = formatHours(minRecentOffset)
\r
397 + (minRecentOffset != maxRecentOffset
\r
398 ? "," + formatHours(maxRecentOffset)
\r
401 return id + getPurportedAliasesAsString() + " (" + hours + ")";
\r
403 // for getting template for translation
\r
404 return "\t{\t\"" + id + "\"\t// [" + count + "] " + hours
\r
405 + getRealAliasesAsString() + "\r\n"
\r
406 + "\t\t// translate the following!!\r\n"
\r
407 + (minRecentOffset != maxRecentOffset
\r
408 ? "\t\t\"" + city + " Standard Time\"\r\n"
\r
409 + "\t\t\"" + city + "-ST\"\r\n"
\r
410 + "\t\t\"" + city + " Daylight Time\"\r\n"
\r
411 + "\t\t\"" + city + "-DT\"\r\n"
\r
416 + "\t\t\"" + city + " Time\"\r\n"
\r
417 + "\t\t\"" + city + "-T\"\r\n"
\r
418 + "\t\t\"" + city + "\"\r\n"
\r