1 package org.unicode.cldr.unittest; 2 3 import java.text.DateFormat; 4 import java.util.ArrayList; 5 import java.util.Date; 6 import java.util.List; 7 import java.util.Locale; 8 import java.util.Map; 9 import java.util.Set; 10 import java.util.TreeMap; 11 import java.util.TreeSet; 12 13 import org.unicode.cldr.util.CldrUtility; 14 import org.unicode.cldr.util.Pair; 15 import org.unicode.cldr.util.SupplementalDataInfo; 16 17 import com.ibm.icu.impl.OlsonTimeZone; 18 import com.ibm.icu.impl.Relation; 19 import com.ibm.icu.text.DecimalFormat; 20 import com.ibm.icu.text.SimpleDateFormat; 21 import com.ibm.icu.util.Calendar; 22 import com.ibm.icu.util.TimeZone; 23 import com.ibm.icu.util.TimeZoneRule; 24 import com.ibm.icu.util.TimeZoneTransition; 25 import com.ibm.icu.util.ULocale; 26 27 public class TestMetazoneTransitions { 28 29 private static final int printDaylightTransitions = 6; 30 31 private static final int SECOND = 1000; 32 private static final int MINUTE = 60 * SECOND; 33 private static final int HOUR = 60 * MINUTE; 34 35 static final long startDate; 36 37 static final long endDate; 38 39 static final SimpleDateFormat neutralFormat = new SimpleDateFormat( 40 "yyyy-MM-dd HH:mm:ss", ULocale.ENGLISH); 41 static final DecimalFormat threeDigits = new DecimalFormat("000"); 42 static final DecimalFormat twoDigits = new DecimalFormat("00"); 43 44 public static final Set<Integer> allOffsets = new TreeSet<Integer>(); 45 46 static { 47 TimeZone GMT = TimeZone.getTimeZone("Etc/GMT"); 48 neutralFormat.setTimeZone(GMT); 49 Calendar cal = Calendar.getInstance(GMT, ULocale.US); 50 int year = cal.get(Calendar.YEAR); cal.clear()51 cal.clear(); // need to clear fractional seconds 52 cal.set(1970, 0, 1, 0, 0, 0); 53 startDate = cal.getTimeInMillis(); 54 cal.set(year + 5, 0, 1, 0, 0, 0); 55 endDate = cal.getTimeInMillis(); 56 if (startDate != 0) { IllegalArgumentException()57 throw new IllegalArgumentException(); 58 } 59 } 60 main(String[] args)61 public static void main(String[] args) throws Exception { 62 java.util.TimeZone zone2 = java.util.TimeZone.getTimeZone("GMT+830"); 63 System.out.println(zone2.getID()); 64 zone2 = java.util.TimeZone.getTimeZone("GMT+08"); 65 System.out.println(zone2.getID()); 66 zone2 = java.util.TimeZone.getTimeZone("Etc/GMT-8"); 67 System.out.println(zone2.getID()); 68 69 java.util.TimeZone.setDefault(java.util.TimeZone 70 .getTimeZone("America/Los_Angeles")); 71 DateFormat javaFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, 72 DateFormat.MEDIUM, Locale.US); 73 long start = new Date(107, 0, 1, 0, 30, 0).getTime(); 74 start -= start % 1000; // clean up millis 75 long end = new Date(108, 0, 1, 0, 30, 0).getTime(); 76 for (long date = start; date < end; date += 15 * MINUTE) { 77 String formatted = javaFormat.format(date); 78 Date roundTrip = javaFormat.parse(formatted); 79 if (roundTrip.getTime() != date) { 80 System.out.println("Java roundtrip failed for: " + formatted 81 + "\tSource: " + new Date(date) + "\tTarget: " 82 + roundTrip); 83 } 84 } 85 new TestMetazoneTransitions().run(); 86 } 87 88 private static class ZoneTransition implements Comparable<ZoneTransition> { 89 long date; 90 91 int offset; 92 equals(Object that)93 public boolean equals(Object that) { 94 ZoneTransition other = (ZoneTransition) that; 95 return date == other.date && offset == other.offset; 96 } 97 hashCode()98 public int hashCode() { 99 return (int) (date ^ (date >>> 32) ^ offset); 100 } 101 ZoneTransition(long date, int offset)102 public ZoneTransition(long date, int offset) { 103 this.date = date; 104 this.offset = offset; 105 } 106 107 /** 108 * Return the one with the smaller offset, or if equal then the smallest 109 * time 110 * 111 * @param o 112 * @return 113 */ compareTo(ZoneTransition o)114 public int compareTo(ZoneTransition o) { 115 int delta = offset - o.offset; 116 if (delta != 0) 117 return delta; 118 long delta2 = date - o.date; 119 return delta2 == 0 ? 0 : delta2 < 0 ? -1 : 1; 120 } 121 122 @Override toString()123 public String toString() { 124 return neutralFormat.format(date) + ": " + ((double) offset) / HOUR 125 + "hrs"; 126 } 127 } 128 129 enum DaylightChoice { 130 NO_DAYLIGHT, ONLY_DAYLIGHT 131 }; 132 133 private static class ZoneTransitions implements Comparable<ZoneTransitions> { 134 List<ZoneTransition> chronologicalList = new ArrayList<ZoneTransition>(); 135 equals(Object that)136 public boolean equals(Object that) { 137 ZoneTransitions other = (ZoneTransitions) that; 138 return chronologicalList.equals(other.chronologicalList); 139 } 140 hashCode()141 public int hashCode() { 142 return chronologicalList.hashCode(); 143 } 144 ZoneTransitions(String tzid, DaylightChoice allowDaylight)145 public ZoneTransitions(String tzid, DaylightChoice allowDaylight) { 146 TimeZone zone = TimeZone.getTimeZone(tzid); 147 for (long date = startDate; date < endDate; date = getTransitionAfter( 148 zone, date)) { 149 addIfDifferent(zone, date, allowDaylight); 150 } 151 } 152 addIfDifferent(TimeZone zone, long date, DaylightChoice allowDaylight)153 private void addIfDifferent(TimeZone zone, long date, 154 DaylightChoice allowDaylight) { 155 int offset = zone.getOffset(date); 156 allOffsets.add(offset); 157 int delta = getDSTSavings(zone, date); 158 switch (allowDaylight) { 159 case ONLY_DAYLIGHT: 160 offset = delta; 161 break; 162 case NO_DAYLIGHT: 163 offset -= delta; 164 break; 165 } 166 int size = chronologicalList.size(); 167 if (size > 0) { 168 ZoneTransition last = chronologicalList.get(size - 1); 169 if (last.offset == offset) { 170 return; 171 } 172 } 173 chronologicalList.add(new ZoneTransition(date, offset)); 174 } 175 compareTo(ZoneTransitions other)176 public int compareTo(ZoneTransitions other) { 177 int minSize = Math.min(chronologicalList.size(), 178 other.chronologicalList.size()); 179 for (int i = 0; i < minSize; ++i) { 180 ZoneTransition a = chronologicalList.get(i); 181 ZoneTransition b = other.chronologicalList.get(i); 182 int order = a.compareTo(b); 183 if (order != 0) 184 return order; 185 } 186 return chronologicalList.size() - other.chronologicalList.size(); 187 } 188 toString(String separator, int abbreviateToSize)189 public String toString(String separator, int abbreviateToSize) { 190 if (abbreviateToSize > 0 191 && chronologicalList.size() > abbreviateToSize) { 192 int limit = abbreviateToSize / 2; 193 return CldrUtility.join(slice(chronologicalList, 0, limit), 194 separator) 195 + separator 196 + "..." 197 + separator 198 + CldrUtility.join( 199 slice(chronologicalList, 200 chronologicalList.size() - limit, 201 chronologicalList.size()), 202 separator); 203 } 204 return CldrUtility.join(chronologicalList, separator); 205 } 206 toString()207 public String toString() { 208 return toString("; ", -1); 209 } 210 size()211 public int size() { 212 // TODO Auto-generated method stub 213 return chronologicalList.size(); 214 } 215 getDifferenceFrom( ZoneTransitions other)216 public Pair<ZoneTransitions, ZoneTransitions> getDifferenceFrom( 217 ZoneTransitions other) { 218 int minSize = Math.min(chronologicalList.size(), 219 other.chronologicalList.size()); 220 for (int i = 0; i < minSize; ++i) { 221 ZoneTransition a = chronologicalList.get(i); 222 ZoneTransition b = other.chronologicalList.get(i); 223 int order = a.compareTo(b); 224 if (order != 0) 225 return new Pair(a, b); 226 } 227 if (chronologicalList.size() > other.chronologicalList.size()) { 228 return new Pair(chronologicalList.get(minSize), null); 229 } else if (chronologicalList.size() < other.chronologicalList 230 .size()) { 231 return new Pair(null, other.chronologicalList.get(minSize)); 232 } else { 233 return new Pair(null, null); 234 } 235 } 236 get(int i)237 public ZoneTransition get(int i) { 238 return chronologicalList.get(i); 239 } 240 } 241 242 final static SupplementalDataInfo supplementalData = SupplementalDataInfo 243 .getInstance("C:/cvsdata/unicode/cldr/common/supplemental/"); 244 run()245 private void run() { 246 // String[] zones = TimeZone.getAvailableIDs(); 247 Relation<ZoneTransitions, String> partition = Relation.of( 248 new TreeMap<ZoneTransitions, Set<String>>(), TreeSet.class); 249 Relation<ZoneTransitions, String> daylightPartition = Relation.of( 250 new TreeMap<ZoneTransitions, Set<String>>(), TreeSet.class); 251 Map<String, String> toDaylight = new TreeMap<String, String>(); 252 Map<ZoneTransitions, String> daylightNames = new TreeMap<ZoneTransitions, String>(); 253 254 // get the main data 255 for (String zone : supplementalData.getCanonicalZones()) { 256 ZoneTransitions transitions = new ZoneTransitions(zone, 257 DaylightChoice.NO_DAYLIGHT); 258 partition.put(transitions, zone); 259 transitions = new ZoneTransitions(zone, 260 DaylightChoice.ONLY_DAYLIGHT); 261 if (transitions.size() > 1) { 262 daylightPartition.put(transitions, zone); 263 } 264 } 265 // now assign names 266 int count = 0; 267 for (ZoneTransitions transitions : daylightPartition.keySet()) { 268 final String dname = "D" + threeDigits.format(++count); 269 daylightNames.put(transitions, dname); 270 for (String zone : daylightPartition.getAll(transitions)) { 271 toDaylight.put(zone, dname); 272 } 273 } 274 // get the "primary" zone for each metazone 275 Map<String, String> zoneToMeta = new TreeMap<String, String>(); 276 Map<String, Map<String, String>> metazoneToRegionToZone = supplementalData 277 .getMetazoneToRegionToZone(); 278 for (String meta : metazoneToRegionToZone.keySet()) { 279 Map<String, String> regionToZone = metazoneToRegionToZone.get(meta); 280 String keyZone = regionToZone.get("001"); 281 zoneToMeta.put(keyZone, meta); 282 } 283 284 System.out.println(); 285 System.out 286 .println("====================================================="); 287 System.out.println("*** Non-Daylight Partition"); 288 System.out 289 .println("====================================================="); 290 System.out.println(); 291 292 count = 0; 293 Set<String> noMeta = new TreeSet<String>(); 294 Set<String> multiMeta = new TreeSet<String>(); 295 Set<String> stableZones = new TreeSet<String>(); 296 for (ZoneTransitions transitions : partition.keySet()) { 297 298 System.out.println(); 299 final String nonDaylightPartitionName = "M" 300 + threeDigits.format(++count); 301 System.out.println("Non-Daylight Partition " 302 + nonDaylightPartitionName); 303 int metaCount = 0; 304 Set<String> metas = new TreeSet<String>(); 305 for (String zone : partition.getAll(transitions)) { 306 String daylightName = toDaylight.get(zone); 307 String meta = zoneToMeta.get(zone); 308 if (meta != null) { 309 ++metaCount; 310 metas.add(meta); 311 } 312 System.out.println("\t" + zone 313 + (daylightName == null ? "" : "\t" + daylightName) 314 + (meta == null ? "" : "\t\tMETA:" + meta)); 315 } 316 if (metaCount == 0) { 317 noMeta.add(nonDaylightPartitionName + "{" 318 + CldrUtility.join(partition.getAll(transitions), ", ") 319 + "}"); 320 } else if (metaCount > 1) { 321 multiMeta.add(nonDaylightPartitionName + "{" 322 + CldrUtility.join(metas, ", ") + "}"); 323 } 324 if (transitions.size() == 1) { 325 final int offset = transitions.get(0).offset; 326 allOffsets.remove(offset); 327 stableZones.add(nonDaylightPartitionName + ", " + offset 328 / (double) HOUR + "hrs " + "{" 329 + CldrUtility.join(partition.getAll(transitions), ", ") 330 + "}"); 331 } 332 System.out.println("\t\t" 333 + transitions.toString(CldrUtility.LINE_SEPARATOR + "\t\t", 334 -1)); 335 } 336 System.out.println(); 337 System.out 338 .println("*** Non-Daylight Partitions with no canonical meta"); 339 System.out.println("\t" 340 + CldrUtility.join(noMeta, CldrUtility.LINE_SEPARATOR + "\t")); 341 System.out.println(); 342 System.out 343 .println("*** Non-Daylight Partitions with more than one canonical meta"); 344 System.out.println("\t" 345 + CldrUtility 346 .join(multiMeta, CldrUtility.LINE_SEPARATOR + "\t")); 347 System.out.println(); 348 System.out.println("*** Stable Non-Daylight Partitions"); 349 System.out.println("\t" 350 + CldrUtility.join(stableZones, CldrUtility.LINE_SEPARATOR 351 + "\t")); 352 System.out.println(); 353 System.out.println("*** Offsets with no stable partition"); 354 for (int offset : allOffsets) { 355 System.out.println("\t" + offset / (double) HOUR + "hrs"); 356 } 357 System.out.println(); 358 359 System.out.println(); 360 System.out 361 .println("====================================================="); 362 System.out.println("*** Daylight Partition"); 363 System.out 364 .println("====================================================="); 365 System.out.println(); 366 367 ZoneTransitions lastTransitions = null; 368 String lastName = null; 369 for (ZoneTransitions transitions : daylightPartition.keySet()) { 370 System.out.println(); 371 String daylightName = daylightNames.get(transitions); 372 System.out.println("Daylight Partition\t" + daylightName); 373 for (String zone : daylightPartition.getAll(transitions)) { 374 System.out.println("\t" + zone); 375 } 376 System.out.println("\t\t" 377 + transitions.toString(CldrUtility.LINE_SEPARATOR + "\t\t", 378 printDaylightTransitions)); 379 if (lastTransitions != null) { 380 Pair<ZoneTransitions, ZoneTransitions> diff = transitions 381 .getDifferenceFrom(lastTransitions); 382 System.out.println("\t\tTransition Difference from " + lastName 383 + ":\t" + diff); 384 } 385 lastTransitions = transitions; 386 lastName = daylightName; 387 } 388 389 } 390 slice(List<T> list, int start, int limit)391 public static <T> List<T> slice(List<T> list, int start, int limit) { 392 ArrayList<T> temp = new ArrayList<T>(); 393 for (int i = start; i < limit; ++i) { 394 temp.add(list.get(i)); 395 } 396 return temp; 397 } 398 399 /* Methods that ought to be on TimeZone */ 400 /** 401 * Return the next point in time after date when the zone has a different 402 * offset than what it has on date. If there are no later transitions, 403 * returns Long.MAX_VALUE. 404 * 405 * @param zone 406 * input zone -- should be method of TimeZone 407 * @param date 408 * input date, in standard millis since 1970-01-01 00:00:00 GMT 409 */ getTransitionAfter(TimeZone zone, long date)410 public static long getTransitionAfter(TimeZone zone, long date) { 411 TimeZoneTransition transition = ((OlsonTimeZone) zone) 412 .getNextTransition(date, false); 413 if (transition == null) { 414 return Long.MAX_VALUE; 415 } 416 date = transition.getTime(); 417 return date; 418 } 419 420 /** 421 * Return true if the zone is in daylight savings on the date. 422 * 423 * @param zone 424 * input zone -- should be method of TimeZone 425 * @param date 426 * input date, in standard millis since 1970-01-01 00:00:00 GMT 427 */ inDaylightTime(TimeZone zone, long date)428 public static boolean inDaylightTime(TimeZone zone, long date) { 429 return ((OlsonTimeZone) zone).inDaylightTime(new Date(date)); 430 } 431 432 /** 433 * Return the daylight savings offset on the given date. 434 * 435 * @param zone 436 * input zone -- should be method of TimeZone 437 * @param date 438 * input date, in standard millis since 1970-01-01 00:00:00 GMT 439 */ getDSTSavings(TimeZone zone, long date)440 public static int getDSTSavings(TimeZone zone, long date) { 441 if (!inDaylightTime(zone, date)) { 442 return 0; 443 } 444 TimeZoneTransition transition = ((OlsonTimeZone) zone) 445 .getPreviousTransition(date + 1, true); 446 TimeZoneRule to = transition.getTo(); 447 int delta = to.getDSTSavings(); 448 // if (delta != HOUR) { 449 // System.out.println("Delta " + delta/(double)HOUR + " for " + 450 // zone.getID()); 451 // } 452 return delta; 453 } 454 }