2 //#if defined(FOUNDATION10) || defined(J2SE13)
5 *******************************************************************************
6 * Copyright (C) 2002-2009, International Business Machines Corporation and *
7 * others. All Rights Reserved. *
8 *******************************************************************************
10 package com.ibm.icu.dev.test.timezone;
12 import java.util.ArrayList;
13 import java.util.Date;
14 import java.util.Calendar;
15 import java.util.GregorianCalendar;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Locale;
21 import java.util.TreeSet;
22 import java.util.Iterator;
24 import java.text.DateFormat;
25 import java.text.NumberFormat;
27 import com.ibm.icu.dev.test.*;
28 import com.ibm.icu.dev.test.util.BagFormatter;
29 import com.ibm.icu.util.TimeZone;
33 * Class for testing TimeZones for consistency
37 public class TimeZoneAliasTest extends TestFmwk {
39 public static void main(String[] args) throws Exception {
40 new TimeZoneAliasTest().run(args);
44 * There are two things to check aliases for:<br>
45 * 1. the alias set must be uniform: if a isAlias b, then aliasSet(a) == aliasSet(b)<br>
46 * 2. all aliases must have the same offsets
48 public void TestAliases() {
49 Zone.Seconds seconds = new Zone.Seconds();
50 for (Iterator it = Zone.getZoneSet().iterator(); it.hasNext(); ) {
51 Zone zone = (Zone)it.next();
53 if (id.indexOf('/') < 0 && (id.endsWith("ST") || id.endsWith("DT"))) {
54 if (zone.minRecentOffset != zone.maxRecentOffset) {
56 "Standard or Daylight Time not constant: " + id
57 + ": " + Zone.formatHours(zone.minRecentOffset)
58 + " != " + Zone.formatHours(zone.maxRecentOffset));
61 Set aliases = zone.getPurportedAliases();
62 Set aliasesSet = new TreeSet(aliases);
63 aliasesSet.add(id); // for comparison
64 Iterator aliasIterator = aliases.iterator();
65 while (aliasIterator.hasNext()) {
66 String otherId = (String)aliasIterator.next();
67 Zone otherZone = Zone.make(otherId);
68 Set otherAliases = otherZone.getPurportedAliases();
69 otherAliases.add(otherId); // for comparison
70 if (!aliasesSet.equals(otherAliases)) {
72 "Aliases Unsymmetric: "
73 + id + " => " + Zone.bf.join(aliasesSet)
75 + otherId + " => " + Zone.bf.join(otherAliases));
77 if (zone.findOffsetOrdering(otherZone, seconds) != 0) {
78 errln("Aliases differ: " + id + ", " + otherId
79 + " differ at " + seconds);
86 * We check to see that every timezone that is not an alias is actually different!
88 public void TestDifferences() {
90 Zone.Seconds diffDate = new Zone.Seconds();
91 for (Iterator it = Zone.getZoneSet().iterator(); it.hasNext();) {
92 Zone testZone = (Zone)it.next();
94 String common = testZone + "\tvs " + last + ":\t";
95 int diff = testZone.findOffsetOrdering(last, diffDate);
97 logln("\t" + common + "difference at: " + diffDate
98 + ", " + Zone.formatHours(diff) + "hr");
99 } else if (testZone.isRealAlias(last)) {
100 logln("\t" + common + "alias, no difference");
102 errln(common + "NOT ALIAS BUT NO DIFFERENCE!");
110 * Utility for printing out zones to be translated.
112 public static void TestGenerateZones() {
114 for (Iterator it = Zone.getUniqueZoneSet().iterator(); it.hasNext();) {
115 Zone zone = (Zone)it.next();
116 System.out.println(zone.toString(count++));
120 /** Utility; ought to be someplace common
123 static String join(Collection c, String separator) {
124 StringBuffer result = new StringBuffer();
125 boolean isFirst = true;
126 for (Iterator it = c.iterator(); it.hasNext(); ) {
127 if (!isFirst) result.append(separator);
128 else isFirst = false;
129 result.append(it.next().toString());
131 return result.toString();
136 * The guts is in this subclass. It sucks in all the data from the zones,
137 * and analyses it. It constructs some mappings for the unique ids,
139 * The main tricky bit is that for performance it pre-analyses all zones
140 * for inflections points; the points in time where the offset changes.
141 * The zones can then be sorted by those points, which allows us to
142 * avoid expensive comparisons.
145 static class Zone implements Comparable {
147 static private final BagFormatter bf = new BagFormatter().setSeparator(", ");
148 static private final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
149 static private final NumberFormat nf = NumberFormat.getInstance(Locale.US);
150 static private final long HOUR = 1000*60*60;
151 static private final double DHOUR = HOUR;
152 static private final long DAY = 24*HOUR;
153 static private final long GROSS_PERIOD = 30*DAY;
154 static private final long EPSILON = HOUR/4;
155 static private final int currentYear = new GregorianCalendar().get(Calendar.YEAR);
156 static private final long endDate = getDate((currentYear+1),0,1).getTime();
157 static private final long endDate2 = getDate((currentYear+1),6,1).getTime();
158 static private final long recentLimit = getDate((currentYear-1),6,1).getTime();
159 static private final long startDate = getDate(1905,0,1).getTime();
161 static private final Map idToZone = new HashMap();
162 static private final Set zoneSet = new TreeSet();
163 static private final Set uniqueZoneSet = new TreeSet();
164 static private final Map idToRealAliases = new HashMap();
166 // build everything once.
168 String [] foo = TimeZone.getAvailableIDs();
169 for (int i = 0; i < foo.length; ++i) {
170 zoneSet.add(Zone.make(foo[i]));
173 Zone.Seconds diffDate = new Zone.Seconds();
174 String lastUnique = "";
175 for (Iterator it = Zone.getZoneSet().iterator(); it.hasNext();) {
176 Zone testZone = (Zone)it.next();
178 uniqueZoneSet.add(testZone);
179 lastUnique = testZone.id;
181 int diff = testZone.findOffsetOrdering(last, diffDate);
183 uniqueZoneSet.add(testZone);
184 lastUnique = testZone.id;
186 Set aliases = (Set)idToRealAliases.get(lastUnique);
187 if (aliases == null) {
188 aliases = new TreeSet();
189 idToRealAliases.put(lastUnique, aliases);
191 aliases.add(testZone.id);
198 static public Set getZoneSet() {
202 public static Set getUniqueZoneSet() {
203 return uniqueZoneSet;
206 static public Zone make(String id) {
207 Zone result = (Zone)idToZone.get(id);
208 if (result != null) return result;
209 result = new Zone(id);
210 idToZone.put(id, result);
214 static public String formatHours(int hours) {
215 return nf.format(hours/DHOUR);
218 // utility class for date return, because Date is clunky.
219 public static class Seconds {
220 public long seconds = Long.MIN_VALUE;
221 public String toString() {
222 if (seconds == Long.MIN_VALUE) return "n/a";
223 return df.format(new Date(seconds));
228 // we keep min/max offsets not only over all time (that we care about)
229 // but also separate ones for recent years.
231 private TimeZone zone;
233 private int minOffset;
234 private int maxOffset;
235 private int minRecentOffset;
236 private int maxRecentOffset;
237 private List inflectionPoints = new ArrayList();
238 private Set purportedAliases = new TreeSet();
240 private Zone(String id) { // for interal use only; use make instead!
241 zone = TimeZone.getTimeZone(id);
245 int equivCount = TimeZone.countEquivalentIDs(id);
246 for (int j = 0; j < equivCount; ++j) {
247 String altID = TimeZone.getEquivalentID(id, j);
248 if (altID.equals(id)) continue;
249 purportedAliases.add(altID);
252 // find inflexion points; times where the offset changed
253 long lastDate = endDate;
254 if (zone.getOffset(lastDate) < zone.getOffset(endDate2)) lastDate = endDate2;
255 maxRecentOffset = minRecentOffset = minOffset = maxOffset = zone.getOffset(lastDate);
257 inflectionPoints.add(new Long(lastDate));
258 int lastOffset = zone.getOffset(endDate);
259 long lastInflection = endDate;
261 // we do a gross search, then narrow in when we find a difference from the last one
262 for (long currentDate = endDate; currentDate >= startDate; currentDate -= GROSS_PERIOD) {
263 int currentOffset = zone.getOffset(currentDate);
264 if (currentOffset != lastOffset) { // Binary Search
265 if (currentOffset < minOffset) minOffset = currentOffset;
266 if (currentOffset > maxOffset) maxOffset = currentOffset;
267 if (lastInflection >= recentLimit) {
268 if (currentOffset < minRecentOffset) minRecentOffset = currentOffset;
269 if (currentOffset > maxRecentOffset) maxRecentOffset = currentOffset;
271 long low = currentDate;
272 long high = lastDate;
273 while (low - high > EPSILON) {
274 long mid = (high + low)/2;
275 int midOffset = zone.getOffset(mid);
276 if (midOffset == low) {
282 inflectionPoints.add(new Long(low));
283 lastInflection = low;
285 lastOffset = currentOffset;
287 inflectionPoints.add(new Long(startDate)); // just to cap it off for comparisons.
290 // we assume that places will not convert time zones then back within one day
291 // so we go first by half
292 public int findOffsetOrdering(Zone other, Seconds dateDiffFound) {
293 //System.out.println("-diff: " + id + "\t" + other.id);
296 int min = inflectionPoints.size();
297 if (other.inflectionPoints.size() < min) min = other.inflectionPoints.size();
300 for (int i = 0; i < min; ++i) {
301 long myIP = ((Long)inflectionPoints.get(i)).longValue();
302 long otherIP = ((Long)other.inflectionPoints.get(i)).longValue();
303 if (myIP > otherIP) { // take lowest, for transitivity (semi)
308 result = zone.getOffset(myIP) - other.zone.getOffset(myIP);
313 if (myIP == otherIP) continue; // test other if different
315 result = zone.getOffset(myIP) - other.zone.getOffset(myIP);
321 // if they are equal so far, we don't care about the rest
323 seconds = Long.MIN_VALUE;
326 //System.out.println("+diff: " + (result/HOUR) + "\t" + dateDiffFound);
327 if (dateDiffFound != null) dateDiffFound.seconds = seconds;
331 // internal buffer to avoid creation all the time.
332 private Seconds diffDateReturn = new Seconds();
334 public int compareTo(Object o) {
335 Zone other = (Zone)o;
336 // first order by max and min offsets
337 // min will usually correspond to standard time, max to daylight
338 // unless there have been historical shifts
339 if (minRecentOffset < other.minRecentOffset) return -1;
340 if (minRecentOffset > other.minRecentOffset) return 1;
341 if (maxRecentOffset < other.maxRecentOffset) return -1;
342 if (maxRecentOffset > other.maxRecentOffset) return 1;
343 // now check that all offsets are the same over history
344 int diffDate = findOffsetOrdering(other, diffDateReturn);
345 if (diffDate != 0) return diffDate;
346 // choose longer name first!!
347 if (id.length() != other.id.length()) {
348 if (id.length() < other.id.length()) return 1;
351 return id.compareTo(other.id);
354 public Set getPurportedAliases() {
355 return new TreeSet(purportedAliases); // clone for safety
358 public boolean isPurportedAlias(String zoneID) {
359 return purportedAliases.contains(zoneID);
362 public boolean isRealAlias(Zone z) {
363 return purportedAliases.contains(z.id);
366 public String getPurportedAliasesAsString() {
367 Set s = getPurportedAliases();
368 if (s.size() == 0) return "";
369 return " " + bf.join(s);
372 public String getRealAliasesAsString() {
373 Set s = (Set)idToRealAliases.get(id);
374 if (s == null) return "";
375 return " *" + bf.join(s);
378 public String getCity() {
379 int pos = id.lastIndexOf(('/'));
380 String city = id.substring(pos+1);
381 return city.replace('_',' ');
384 public String toString() {
389 * Where count > 0, returns string that is set up for translation
391 public String toString(int count) {
392 String city = getCity();
393 String hours = formatHours(minRecentOffset)
394 + (minRecentOffset != maxRecentOffset
395 ? "," + formatHours(maxRecentOffset)
398 return id + getPurportedAliasesAsString() + " (" + hours + ")";
400 // for getting template for translation
401 return "\t{\t\"" + id + "\"\t// [" + count + "] " + hours
402 + getRealAliasesAsString() + "\r\n"
403 + "\t\t// translate the following!!\r\n"
404 + (minRecentOffset != maxRecentOffset
405 ? "\t\t\"" + city + " Standard Time\"\r\n"
406 + "\t\t\"" + city + "-ST\"\r\n"
407 + "\t\t\"" + city + " Daylight Time\"\r\n"
408 + "\t\t\"" + city + "-DT\"\r\n"
413 + "\t\t\"" + city + " Time\"\r\n"
414 + "\t\t\"" + city + "-T\"\r\n"
415 + "\t\t\"" + city + "\"\r\n"