/**
 *******************************************************************************
 * Copyright (C) 2000-2011, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */

package com.ibm.icu.dev.test.timezone;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;

/**
 * @test 1.22 99/09/21
 * @bug 4028006 4044013 4096694 4107276 4107570 4112869 4130885
 * @summary test TimeZone
 * @build TimeZoneTest
 */
public class TimeZoneTest extends TestFmwk
{
    static final int millisPerHour = 3600000;

    // TODO: We should probably read following data at runtime, so we can update
    // the these values every release with necessary data changes.

    // Some test case data is current date/tzdata version sensitive and producing errors
    // when year/rule are changed.
    static final int REFERENCE_YEAR = 2009;
    static final String REFERENCE_DATA_VERSION = "2009d";



    public static void main(String[] args) throws Exception {
        new TimeZoneTest().run(args);
    }

    /**
     * NOTE: As of ICU 2.8, the mapping of 3-letter legacy aliases
     * to `real' Olson IDs is under control of the underlying JDK.
     * This test may fail on one JDK and pass on another; don't be
     * too concerned.  Alan
     *
     * Bug 4130885
     * Certain short zone IDs, used since 1.1.x, are incorrect.
     *  
     * The worst of these is:
     *
     * "CAT" (Central African Time) should be GMT+2:00, but instead returns a
     * zone at GMT-1:00. The zone at GMT-1:00 should be called EGT, CVT, EGST,
     * or AZOST, depending on which zone is meant, but in no case is it CAT.
     *
     * Other wrong zone IDs:
     *
     * ECT (European Central Time) GMT+1:00: ECT is Ecuador Time,
     * GMT-5:00. European Central time is abbreviated CEST.
     *
     * SST (Solomon Island Time) GMT+11:00. SST is actually Samoa Standard Time,
     * GMT-11:00. Solomon Island time is SBT.
     *
     * NST (New Zealand Time) GMT+12:00. NST is the abbreviation for
     * Newfoundland Standard Time, GMT-3:30. New Zealanders use NZST.
     *
     * AST (Alaska Standard Time) GMT-9:00. [This has already been noted in
     * another bug.] It should be "AKST". AST is Atlantic Standard Time,
     * GMT-4:00.
     *
     * PNT (Phoenix Time) GMT-7:00. PNT usually means Pitcairn Time,
     * GMT-8:30. There is no standard abbreviation for Phoenix time, as distinct
     * from MST with daylight savings.
     *
     * In addition to these problems, a number of zones are FAKE. That is, they
     * don't match what people use in the real world.
     *
     * FAKE zones:
     *
     * EET (should be EEST)
     * ART (should be EEST)
     * MET (should be IRST)
     * NET (should be AMST)
     * PLT (should be PKT)
     * BST (should be BDT)
     * VST (should be ICT)
     * CTT (should be CST) +
     * ACT (should be CST) +
     * AET (should be EST) +
     * MIT (should be WST) +
     * IET (should be EST) +
     * PRT (should be AST) +
     * CNT (should be NST)
     * AGT (should be ARST)
     * BET (should be EST) +
     *
     * + A zone with the correct name already exists and means something
     * else. E.g., EST usually indicates the US Eastern zone, so it cannot be
     * used for Brazil (BET).
     */
    public void TestShortZoneIDs() throws Exception {

        // This test case is tzdata version sensitive.
        boolean isNonReferenceTzdataVersion = false;
        String tzdataVer = TimeZone.getTZDataVersion();
        if (!tzdataVer.equals(REFERENCE_DATA_VERSION)) {
            // Note: We want to display a warning message here if
            // REFERENCE_DATA_VERSION is out of date - so we
            // do not forget to update the value before GA.
            isNonReferenceTzdataVersion = true;
            logln("Warning: Active tzdata version (" + tzdataVer +
                    ") does not match the reference tzdata version ("
                    + REFERENCE_DATA_VERSION + ") for this test case data.");
        }

        // Note: If the default TimeZone type is JDK, some time zones
        // may differ from the test data below.  For example, "MST" on
        // IBM JRE is an alias of "America/Denver" for supporting Java 1.1
        // backward compatibility, while Olson tzdata (and ICU) treat it
        // as -7hour fixed offset/no DST.
        boolean isJDKTimeZone = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_JDK);
        if (isJDKTimeZone) {
            logln("Warning: Using JDK TimeZone.  Some test cases may not return expected results.");
        }

        // Note: useDaylightTime returns true if DST is observed
        // in the time zone in the current calendar year.  The test
        // data is valid for the date after the reference year below.
        // If system clock is before the year, some test cases may
        // fail.
        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/GMT"));
        cal.set(REFERENCE_YEAR, Calendar.JANUARY, 2); // day 2 in GMT

        boolean isDateBeforeReferenceYear = System.currentTimeMillis() < cal.getTimeInMillis();
        if (isDateBeforeReferenceYear) {
            logln("Warning: Past time is set to the system clock.  Some test cases may not return expected results.");
        }

        ZoneDescriptor[] REFERENCE_LIST = {
            new ZoneDescriptor("MIT", -660, false),
            new ZoneDescriptor("HST", -600, false),
            new ZoneDescriptor("AST", -540, true),
            new ZoneDescriptor("PST", -480, true),
            new ZoneDescriptor("PNT", -420, false),
            new ZoneDescriptor("MST", -420, false),// updated Aug 2003 aliu
            new ZoneDescriptor("CST", -360, true),
            new ZoneDescriptor("IET", -300, true), // updated Feb 2006 srl
            new ZoneDescriptor("EST", -300, false),// updated Aug 2003 aliu
            new ZoneDescriptor("PRT", -240, false),
            new ZoneDescriptor("CNT", -210, true),
            new ZoneDescriptor("AGT", -180, true), // updated by tzdata 2007k
            new ZoneDescriptor("BET", -180, true),
            new ZoneDescriptor("GMT", 0, false),
            new ZoneDescriptor("UTC", 0, false),
            new ZoneDescriptor("ECT", 60, true),
            new ZoneDescriptor("MET", 60, true),
            new ZoneDescriptor("CAT", 120, false), // Africa/Harare
            new ZoneDescriptor("ART", 120, true),
            new ZoneDescriptor("EET", 120, true),
            new ZoneDescriptor("EAT", 180, false),
            new ZoneDescriptor("NET", 240, true),
            new ZoneDescriptor("PLT", 300, false), // updated by tzdata 2008c - no DST after 2008
            new ZoneDescriptor("IST", 330, false),
            new ZoneDescriptor("BST", 360, false),
            new ZoneDescriptor("VST", 420, false),
            new ZoneDescriptor("CTT", 480, false), // updated Oct 2003 aliu
            new ZoneDescriptor("JST", 540, false),
            new ZoneDescriptor("ACT", 570, false), // updated Oct 2003 aliu
            new ZoneDescriptor("AET", 600, true),
            new ZoneDescriptor("SST", 660, false),
            new ZoneDescriptor("NST", 720, true), // Pacific/Auckland

            new ZoneDescriptor("Etc/Unknown", 0, false),
            new ZoneDescriptor("SystemV/AST4ADT", -240, true),
            new ZoneDescriptor("SystemV/EST5EDT", -300, true),
            new ZoneDescriptor("SystemV/CST6CDT", -360, true),
            new ZoneDescriptor("SystemV/MST7MDT", -420, true),
            new ZoneDescriptor("SystemV/PST8PDT", -480, true),
            new ZoneDescriptor("SystemV/YST9YDT", -540, true),
            new ZoneDescriptor("SystemV/AST4", -240, false),
            new ZoneDescriptor("SystemV/EST5", -300, false),
            new ZoneDescriptor("SystemV/CST6", -360, false),
            new ZoneDescriptor("SystemV/MST7", -420, false),
            new ZoneDescriptor("SystemV/PST8", -480, false),
            new ZoneDescriptor("SystemV/YST9", -540, false),
            new ZoneDescriptor("SystemV/HST10", -600, false),
        };

        for (int i=0; i<REFERENCE_LIST.length; ++i) {
            ZoneDescriptor referenceZone = REFERENCE_LIST[i];
            ZoneDescriptor currentZone = new ZoneDescriptor(TimeZone.getTimeZone(referenceZone.getID()));
            if (referenceZone.equals(currentZone)) {
                logln("ok " + referenceZone);
            }
            else {
                if (isNonReferenceTzdataVersion
                        || isJDKTimeZone || isDateBeforeReferenceYear) {
                    logln("Warning: Expected " + referenceZone +
                            "; got " + currentZone);
                } else {
                    errln("Fail: Expected " + referenceZone +
                            "; got " + currentZone);
                }
            }
        }
    }

    /**
     * A descriptor for a zone; used to regress the short zone IDs.
     */
    static class ZoneDescriptor {
        String id;
        int offset; // In minutes
        boolean daylight;

        ZoneDescriptor(TimeZone zone) {
            this.id = zone.getID();
            this.offset = zone.getRawOffset() / 60000;
            this.daylight = zone.useDaylightTime();
        }

        ZoneDescriptor(String id, int offset, boolean daylight) {
            this.id = id;
            this.offset = offset;
            this.daylight = daylight;
        }

        public String getID() { return id; }

        public boolean equals(Object o) {
            ZoneDescriptor that = (ZoneDescriptor)o;
            return that != null &&
                id.equals(that.id) &&
                offset == that.offset &&
                daylight == that.daylight;
        }

        public String toString() {
            int min = offset;
            char sign = '+';
            if (min < 0) { sign = '-'; min = -min; }

            return "Zone[\"" + id + "\", GMT" + sign + (min/60) + ':' +
                (min%60<10?"0":"") + (min%60) + ", " +
                (daylight ? "Daylight" : "Standard") + "]";
        }

        public static int compare(Object o1, Object o2) {
            ZoneDescriptor i1 = (ZoneDescriptor)o1;
            ZoneDescriptor i2 = (ZoneDescriptor)o2;
            if (i1.offset > i2.offset) return 1;
            if (i1.offset < i2.offset) return -1;
            if (i1.daylight && !i2.daylight) return 1;
            if (!i1.daylight && i2.daylight) return -1;
            return i1.id.compareTo(i2.id);
        }
    }

    /**
     * As part of the VM fix (see CCC approved RFE 4028006, bug
     * 4044013), TimeZone.getTimeZone() has been modified to recognize
     * generic IDs of the form GMT[+-]hh:mm, GMT[+-]hhmm, and
     * GMT[+-]hh.  Test this behavior here.
     *
     * Bug 4044013
     */
    public void TestCustomParse() {
        String[] DATA = {
            // ID               offset(sec)     output ID
            "GMT",              "0",            "GMT",      // system ID
            "GMT-YOUR.AD.HERE", "0",            "GMT",
            "GMT0",             "0",            "GMT0",     // system ID
            "GMT+0",            "0",            "GMT+0",    // system ID
            "GMT+1",            "3600",         "GMT+01:00",
            "GMT-0030",         "-1800",        "GMT-00:30",
            "GMT+15:99",        "0",            "GMT",
            "GMT+",             "0",            "GMT",
            "GMT-",             "0",            "GMT",
            "GMT+0:",           "0",            "GMT",
            "GMT-:",            "0",            "GMT",
            "GMT+0010",         "600",          "GMT+00:10",
            "GMT-10",           "-36000",       "GMT-10:00",
            "GMT+30",           "0",            "GMT",
            "GMT-3:30",         "-12600",       "GMT-03:30",
            "GMT-230",          "-9000",        "GMT-02:30",
            "GMT+05:13:05",     "18785",        "GMT+05:13:05",
            "GMT-71023",        "-25823",       "GMT-07:10:23",
            "GMT+01:23:45:67",  "0",            "GMT",
            "GMT+01:234",       "0",            "GMT",
            "GMT-2:31:123",     "0",            "GMT",
            "GMT+3:75",         "0",            "GMT",
            "GMT-01010101",     "0",            "GMT",
        };
        for (int i = 0; i < DATA.length; i += 3) {
            String id = DATA[i];
            int offset = Integer.parseInt(DATA[i+1]);
            String expId = DATA[i+2];

            TimeZone zone = TimeZone.getTimeZone(id);
            String gotID = zone.getID();
            int gotOffset = zone.getRawOffset()/1000;

            logln(id + " -> " + gotID + " " + gotOffset);

            if (offset != gotOffset) {
                errln("FAIL: Unexpected offset for " + id + " - returned:" + gotOffset + " expected:" + offset);
            }
            if (!expId.equals(gotID)) {
                if (TimeZone.getDefaultTimeZoneType() != TimeZone.TIMEZONE_ICU) {
                    logln("ID for " + id + " - returned:" + gotID + " expected:" + expId);
                } else {
                    errln("FAIL: Unexpected ID for " + id + " - returned:" + gotID + " expected:" + expId);
                }
            }
        }
    }

    /**
     * Test the basic functionality of the getDisplayName() API.
     *
     * Bug 4112869
     * Bug 4028006
     *
     * See also API change request A41.
     *
     * 4/21/98 - make smarter, so the test works if the ext resources
     * are present or not.
     */
    public void TestDisplayName() {
        TimeZone zone = TimeZone.getTimeZone("PST");
        String name = zone.getDisplayName(Locale.ENGLISH);
        logln("PST->" + name);

        // dlf - we now (3.4.1) return generic time
        if (!name.equals("Pacific Time"))
            errln("Fail: Expected \"Pacific Time\", got " + name +
                  " for " + zone);

        //*****************************************************************
        // THE FOLLOWING LINES MUST BE UPDATED IF THE LOCALE DATA CHANGES
        // THE FOLLOWING LINES MUST BE UPDATED IF THE LOCALE DATA CHANGES
        // THE FOLLOWING LINES MUST BE UPDATED IF THE LOCALE DATA CHANGES
        //*****************************************************************

        // Test to allow the user to choose to get all the forms 
        // (z, zzzz, Z, ZZZZ, v, vvvv)
        // todo: check to see whether we can test for all of pst, pdt, pt
        Object[] DATA = {
            // z and zzzz
            Boolean.FALSE, new Integer(TimeZone.SHORT), "PST",
            Boolean.TRUE,  new Integer(TimeZone.SHORT), "PDT",
            Boolean.FALSE, new Integer(TimeZone.LONG),  "Pacific Standard Time",
            Boolean.TRUE,  new Integer(TimeZone.LONG),  "Pacific Daylight Time",                
            // v and vvvv
            Boolean.FALSE, new Integer(TimeZone.SHORT_GENERIC), "PT",
            Boolean.TRUE,  new Integer(TimeZone.SHORT_GENERIC), "PT",
            Boolean.FALSE, new Integer(TimeZone.LONG_GENERIC),  "Pacific Time",
            Boolean.TRUE,  new Integer(TimeZone.LONG_GENERIC),  "Pacific Time",  
            // z and ZZZZ
            Boolean.FALSE, new Integer(TimeZone.SHORT_GMT), "-0800",
            Boolean.TRUE,  new Integer(TimeZone.SHORT_GMT), "-0700",
            Boolean.FALSE, new Integer(TimeZone.LONG_GMT),  "GMT-08:00",
            Boolean.TRUE,  new Integer(TimeZone.LONG_GMT),  "GMT-07:00",              
            // V and VVVV
            Boolean.FALSE, new Integer(TimeZone.SHORT_COMMONLY_USED), "PST",
            Boolean.TRUE,  new Integer(TimeZone.SHORT_COMMONLY_USED), "PDT",
            Boolean.FALSE, new Integer(TimeZone.GENERIC_LOCATION),  "United States (Los Angeles)",
            Boolean.TRUE,  new Integer(TimeZone.GENERIC_LOCATION),  "United States (Los Angeles)",
        };

        for (int i=0; i<DATA.length; i+=3) {
            name = zone.getDisplayName(((Boolean)DATA[i]).booleanValue(),
                                       ((Integer)DATA[i+1]).intValue(),
                                       Locale.ENGLISH);
            if (!name.equals(DATA[i+2]))
                errln("Fail: Expected " + DATA[i+2] + "; got " + name);
        }

        // Make sure that we don't display the DST name by constructing a fake
        // PST zone that has DST all year long.
        // dlf - this test is no longer relevant, we display generic time now
        //    so the behavior of the timezone doesn't matter
        SimpleTimeZone zone2 = new SimpleTimeZone(0, "PST");
        zone2.setStartRule(Calendar.JANUARY, 1, 0);
        zone2.setEndRule(Calendar.DECEMBER, 31, 86399999);
        logln("Modified PST inDaylightTime->" + zone2.inDaylightTime(new Date()));
        name = zone2.getDisplayName(Locale.ENGLISH);
        logln("Modified PST->" + name);
        if (!name.equals("Pacific Time"))
            errln("Fail: Expected \"Pacific Time\"");

        // Make sure we get the default display format for Locales
        // with no display name data.
        Locale mt_MT = new Locale("mt", "MT");
        name = zone.getDisplayName(mt_MT);
        //*****************************************************************
        // THE FOLLOWING LINE MUST BE UPDATED IF THE LOCALE DATA CHANGES
        // THE FOLLOWING LINE MUST BE UPDATED IF THE LOCALE DATA CHANGES
        // THE FOLLOWING LINE MUST BE UPDATED IF THE LOCALE DATA CHANGES
        //*****************************************************************
        logln("PST(mt_MT)->" + name);

        // Now be smart -- check to see if zh resource is even present.
        // If not, we expect the en fallback behavior.

        // in icu4j 2.1 we know we have the zh_CN locale data, though it's incomplete
//    /"DateFormatZoneData", 
        UResourceBundle enRB = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME,Locale.ENGLISH);
        UResourceBundle mtRB = UResourceBundle.getBundleInstance(ICUResourceBundle.ICU_BASE_NAME, mt_MT);
        boolean noZH = enRB == mtRB;

        if (noZH) {
            logln("Warning: Not testing the mt_MT behavior because resource is absent");
            if (!name.equals("Pacific Standard Time"))
                errln("Fail: Expected Pacific Standard Time for PST in mt_MT but got ");
        }
        // dlf - we will use generic time, or if unavailable, GMT for standard time in the zone 
        //     - we now (3.4.1) have localizations for this zone, so change test string
        else if(!name.equals("Stati Uniti (Los Angeles)") &&
            !name.equals("GMT-08:00") &&
            !name.equals("GMT-8:00") &&
            !name.equals("GMT-0800") &&
            !name.equals("GMT-800")) {

            errln("Fail: got '" + name + "', expected GMT-08:00 or something similar\n" +
                  "************************************************************\n" +
                  "THE ABOVE FAILURE MAY JUST MEAN THE LOCALE DATA HAS CHANGED\n" +
                  "************************************************************");
        }
        
        // Now try a non-existent zone
        zone2 = new SimpleTimeZone(90*60*1000, "xyzzy");
        name = zone2.getDisplayName(Locale.ENGLISH);
        logln("GMT+90min->" + name);
        if (!name.equals("GMT+01:30") &&
            !name.equals("GMT+1:30") &&
            !name.equals("GMT+0130") &&
            !name.equals("GMT+130"))
            errln("Fail: Expected GMT+01:30 or something similar");
        
        // cover getDisplayName() - null arg
        ULocale save = ULocale.getDefault();
        ULocale.setDefault(ULocale.US);
        name = zone2.getDisplayName();
        logln("GMT+90min->" + name + "for default display locale");
        if (!name.equals("GMT+01:30") &&
            !name.equals("GMT+1:30") &&
            !name.equals("GMT+0130") &&
            !name.equals("GMT+130"))
            errln("Fail: Expected GMT+01:30 or something similar");        
        ULocale.setDefault(save);
 
    }


    public void TestDisplayName2() {
        Date now = new Date();

        String[] timezones = {"America/Chicago", "Europe/Moscow", "Europe/Rome", "Asia/Shanghai", "WET" };
        String[] locales = {"en", "fr", "de", "ja", "zh_TW", "zh_Hans" };
        for (int j = 0; j < locales.length; ++j) {
            ULocale locale = new ULocale(locales[j]);
            for (int i = 0; i < timezones.length; ++i) {
                TimeZone tz = TimeZone.getTimeZone(timezones[i]);
                String displayName0 = tz.getDisplayName(locale); // doesn't work???
                SimpleDateFormat dt = new SimpleDateFormat("vvvv", locale);
                dt.setTimeZone(tz);
                String displayName1 = dt.format(now);  // date value _does_ matter if we fallback to GMT
                logln(locale.getDisplayName() + ", " + tz.getID() + ": " + displayName0);
                if (!displayName1.equals(displayName0)) {
                    errln(locale.getDisplayName() + ", " + tz.getID() + 
                          ": expected " + displayName1 + " but got: " + displayName0);
                }
            }
        }
    }

    public void TestGenericAPI() {
        String id = "NewGMT";
        int offset = 12345;

        SimpleTimeZone zone = new SimpleTimeZone(offset, id);
        if (zone.useDaylightTime()) errln("FAIL: useDaylightTime should return false");

        TimeZone zoneclone = (TimeZone)zone.clone();
        if (!zoneclone.equals(zone)) errln("FAIL: clone or operator== failed");
        zoneclone.setID("abc");
        if (zoneclone.equals(zone)) errln("FAIL: clone or operator!= failed");
        // delete zoneclone;

        zoneclone = (TimeZone)zone.clone();
        if (!zoneclone.equals(zone)) errln("FAIL: clone or operator== failed");
        zoneclone.setRawOffset(45678);
        if (zoneclone.equals(zone)) errln("FAIL: clone or operator!= failed");

        // C++ only
        /*
          SimpleTimeZone copy(*zone);
          if (!(copy == *zone)) errln("FAIL: copy constructor or operator== failed");
          copy = *(SimpleTimeZone*)zoneclone;
          if (!(copy == *zoneclone)) errln("FAIL: assignment operator or operator== failed");
          */

        TimeZone saveDefault = TimeZone.getDefault();
        TimeZone.setDefault(zone);
        TimeZone defaultzone = TimeZone.getDefault();
        if (defaultzone == zone) errln("FAIL: Default object is identical, not clone");
        if (!defaultzone.equals(zone)) errln("FAIL: Default object is not equal");
        TimeZone.setDefault(saveDefault);
        // delete defaultzone;
        // delete zoneclone;

//      // ICU 2.6 Coverage
//      logln(zone.toString());
//      logln(zone.getDisplayName());
//      SimpleTimeZoneAdapter stza = new SimpleTimeZoneAdapter((SimpleTimeZone) TimeZone.getTimeZone("GMT"));
//      stza.setID("Foo");
//      if (stza.hasSameRules(java.util.TimeZone.getTimeZone("GMT"))) {
//          errln("FAIL: SimpleTimeZoneAdapter.hasSameRules");
//      }
//      stza.setRawOffset(3000);
//      offset = stza.getOffset(GregorianCalendar.BC, 2001, Calendar.DECEMBER,
//                              25, Calendar.TUESDAY, 12*60*60*1000);
//      if (offset != 3000) {
//          errln("FAIL: SimpleTimeZoneAdapter.getOffset");
//      }
//      SimpleTimeZoneAdapter dup = (SimpleTimeZoneAdapter) stza.clone();
//      if (stza.hashCode() != dup.hashCode()) {
//          errln("FAIL: SimpleTimeZoneAdapter.hashCode");
//      }
//      if (!stza.equals(dup)) {
//          errln("FAIL: SimpleTimeZoneAdapter.equals");
//      }
//      logln(stza.toString());

        String tzver = TimeZone.getTZDataVersion();
        if (tzver.length() != 5 /* 4 digits + 1 letter */) {
            errln("FAIL: getTZDataVersion returned " + tzver);
        } else {
            logln("PASS: tzdata version: " + tzver);
        }
    }

    public void TestRuleAPI()
    {
        // ErrorCode status = ZERO_ERROR;

        int offset = (int)(60*60*1000*1.75); // Pick a weird offset
        SimpleTimeZone zone = new SimpleTimeZone(offset, "TestZone");
        if (zone.useDaylightTime()) errln("FAIL: useDaylightTime should return false");

        // Establish our expected transition times.  Do this with a non-DST
        // calendar with the (above) declared local offset.
        GregorianCalendar gc = new GregorianCalendar(zone);
        gc.clear();
        gc.set(1990, Calendar.MARCH, 1);
        long marchOneStd = gc.getTime().getTime(); // Local Std time midnight
        gc.clear();
        gc.set(1990, Calendar.JULY, 1);
        long julyOneStd = gc.getTime().getTime(); // Local Std time midnight

        // Starting and ending hours, WALL TIME
        int startHour = (int)(2.25 * 3600000);
        int endHour   = (int)(3.5  * 3600000);

        zone.setStartRule(Calendar.MARCH, 1, 0, startHour);
        zone.setEndRule  (Calendar.JULY,  1, 0, endHour);

        gc = new GregorianCalendar(zone);
        // if (failure(status, "new GregorianCalendar")) return;

        long marchOne = marchOneStd + startHour;
        long julyOne = julyOneStd + endHour - 3600000; // Adjust from wall to Std time

        long expMarchOne = 636251400000L;
        if (marchOne != expMarchOne)
        {
            errln("FAIL: Expected start computed as " + marchOne +
                  " = " + new Date(marchOne));
            logln("      Should be                  " + expMarchOne +
                  " = " + new Date(expMarchOne));
        }

        long expJulyOne = 646793100000L;
        if (julyOne != expJulyOne)
        {
            errln("FAIL: Expected start computed as " + julyOne +
                  " = " + new Date(julyOne));
            logln("      Should be                  " + expJulyOne +
                  " = " + new Date(expJulyOne));
        }

        Calendar cal1 = Calendar.getInstance();
        cal1.set(1990, Calendar.JANUARY, 1);
        Calendar cal2 = Calendar.getInstance();
        cal2.set(1990, Calendar.JUNE, 1);
        _testUsingBinarySearch(zone, cal1.getTimeInMillis(),
                               cal2.getTimeInMillis(), marchOne);
        cal1.set(1990, Calendar.JUNE, 1);
        cal2.set(1990, Calendar.DECEMBER, 31);
        _testUsingBinarySearch(zone, cal1.getTimeInMillis(),
                               cal2.getTimeInMillis(), julyOne);

        if (zone.inDaylightTime(new Date(marchOne - 1000)) ||
            !zone.inDaylightTime(new Date(marchOne)))
            errln("FAIL: Start rule broken");
        if (!zone.inDaylightTime(new Date(julyOne - 1000)) ||
            zone.inDaylightTime(new Date(julyOne)))
            errln("FAIL: End rule broken");

        zone.setStartYear(1991);
        if (zone.inDaylightTime(new Date(marchOne)) ||
            zone.inDaylightTime(new Date(julyOne - 1000)))
            errln("FAIL: Start year broken");

        // failure(status, "TestRuleAPI");
        // delete gc;
        // delete zone;
    }

    void _testUsingBinarySearch(SimpleTimeZone tz, long min, long max, long expectedBoundary)
    {
        // ErrorCode status = ZERO_ERROR;
        boolean startsInDST = tz.inDaylightTime(new Date(min));
        // if (failure(status, "SimpleTimeZone::inDaylightTime")) return;
        if (tz.inDaylightTime(new Date(max)) == startsInDST) {
            logln("Error: inDaylightTime(" + new Date(max) + ") != " + (!startsInDST));
            return;
        }
        // if (failure(status, "SimpleTimeZone::inDaylightTime")) return;
        while ((max - min) > INTERVAL) {
            long mid = (min + max) / 2;
            if (tz.inDaylightTime(new Date(mid)) == startsInDST) {
                min = mid;
            }
            else {
                max = mid;
            }
            // if (failure(status, "SimpleTimeZone::inDaylightTime")) return;
        }
        logln("Binary Search Before: " + min + " = " + new Date(min));
        logln("Binary Search After:  " + max + " = " + new Date(max));
        long mindelta = expectedBoundary - min;
        // not used long maxdelta = max - expectedBoundary;
        if (mindelta >= 0 &&
            mindelta <= INTERVAL &&
            mindelta >= 0 &&
            mindelta <= INTERVAL)
            logln("PASS: Expected bdry:  " + expectedBoundary + " = " + new Date(expectedBoundary));
        else
            errln("FAIL: Expected bdry:  " + expectedBoundary + " = " + new Date(expectedBoundary));
    }

    static final int INTERVAL = 100;

    // Bug 006; verify the offset for a specific zone.
    public void TestPRTOffset()
    {
        TimeZone tz = TimeZone.getTimeZone( "PRT" );
        if( tz == null ) {
            errln( "FAIL: TimeZone(PRT) is null" );
        }
        else{
            if (tz.getRawOffset() != (-4*millisPerHour))
                warnln("FAIL: Offset for PRT should be -4, got " +
                      tz.getRawOffset() / (double)millisPerHour);
        }

    }

    // Test various calls
    public void TestVariousAPI518()
    {
        TimeZone time_zone = TimeZone.getTimeZone("PST");
        Calendar cal = Calendar.getInstance();
        cal.set(1997, Calendar.APRIL, 30);
        Date d = cal.getTime();

        logln("The timezone is " + time_zone.getID());

        if (time_zone.inDaylightTime(d) != true)
            errln("FAIL: inDaylightTime returned false");

        if (time_zone.useDaylightTime() != true)
            errln("FAIL: useDaylightTime returned false");

        if (time_zone.getRawOffset() != -8*millisPerHour)
            errln( "FAIL: getRawOffset returned wrong value");

        GregorianCalendar gc = new GregorianCalendar();
        gc.setTime(d);
        if (time_zone.getOffset(GregorianCalendar.AD, gc.get(GregorianCalendar.YEAR), gc.get(GregorianCalendar.MONTH),
                                gc.get(GregorianCalendar.DAY_OF_MONTH),
                                gc.get(GregorianCalendar.DAY_OF_WEEK), 0)
            != -7*millisPerHour)
            errln("FAIL: getOffset returned wrong value");
    }

    // Test getAvailableID API
    public void TestGetAvailableIDs913()
    {
        StringBuffer buf = new StringBuffer("TimeZone.getAvailableIDs() = { ");
        String[] s = TimeZone.getAvailableIDs();
        for (int i=0; i<s.length; ++i)
        {
            if (i > 0) buf.append(", ");
            buf.append(s[i]);
        }
        buf.append(" };");
        logln(buf.toString());

        buf.setLength(0);
        buf.append("TimeZone.getAvailableIDs(GMT+02:00) = { ");
        s = TimeZone.getAvailableIDs(+2 * 60 * 60 * 1000);
        for (int i=0; i<s.length; ++i)
        {
            if (i > 0) buf.append(", ");
            buf.append(s[i]);
        }
        buf.append(" };");
        logln(buf.toString());

        TimeZone tz = TimeZone.getTimeZone("PST");
        if (tz != null)
            logln("getTimeZone(PST) = " + tz.getID());
        else
            errln("FAIL: getTimeZone(PST) = null");

        tz = TimeZone.getTimeZone("America/Los_Angeles");
        if (tz != null)
            logln("getTimeZone(America/Los_Angeles) = " + tz.getID());
        else
            errln("FAIL: getTimeZone(PST) = null");

        // Bug 4096694
        tz = TimeZone.getTimeZone("NON_EXISTENT");
        if (tz == null)
            errln("FAIL: getTimeZone(NON_EXISTENT) = null");
        else if (!tz.getID().equals("GMT"))
            errln("FAIL: getTimeZone(NON_EXISTENT) = " + tz.getID());
    }

    /**
     * Bug 4107276
     */
    public void TestDSTSavings() {
        // It might be better to find a way to integrate this test into the main TimeZone
        // tests above, but I don't have time to figure out how to do this (or if it's
        // even really a good idea).  Let's consider that a future.  --rtg 1/27/98
        SimpleTimeZone tz = new SimpleTimeZone(-5 * millisPerHour, "dstSavingsTest",
                                               Calendar.MARCH, 1, 0, 0, Calendar.SEPTEMBER, 1, 0, 0,
                                               (int)(0.5 * millisPerHour));

        if (tz.getRawOffset() != -5 * millisPerHour)
            errln("Got back a raw offset of " + (tz.getRawOffset() / millisPerHour) +
                  " hours instead of -5 hours.");
        if (!tz.useDaylightTime())
            errln("Test time zone should use DST but claims it doesn't.");
        if (tz.getDSTSavings() != 0.5 * millisPerHour)
            errln("Set DST offset to 0.5 hour, but got back " + (tz.getDSTSavings() /
                                                                 millisPerHour) + " hours instead.");

        int offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.JANUARY, 1,
                                  Calendar.THURSDAY, 10 * millisPerHour);
        if (offset != -5 * millisPerHour)
            errln("The offset for 10 AM, 1/1/98 should have been -5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.JUNE, 1, Calendar.MONDAY,
                              10 * millisPerHour);
        if (offset != -4.5 * millisPerHour)
            errln("The offset for 10 AM, 6/1/98 should have been -4.5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        tz.setDSTSavings(millisPerHour);
        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.JANUARY, 1,
                              Calendar.THURSDAY, 10 * millisPerHour);
        if (offset != -5 * millisPerHour)
            errln("The offset for 10 AM, 1/1/98 should have been -5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.JUNE, 1, Calendar.MONDAY,
                              10 * millisPerHour);
        if (offset != -4 * millisPerHour)
            errln("The offset for 10 AM, 6/1/98 (with a 1-hour DST offset) should have been -4 hours, but we got "
                  + (offset / millisPerHour) + " hours.");
    }

    /**
     * Bug 4107570
     */
    public void TestAlternateRules() {
        // Like TestDSTSavings, this test should probably be integrated somehow with the main
        // test at the top of this class, but I didn't have time to figure out how to do that.
        //                      --rtg 1/28/98

        SimpleTimeZone tz = new SimpleTimeZone(-5 * millisPerHour, "alternateRuleTest");

        // test the day-of-month API
        tz.setStartRule(Calendar.MARCH, 10, 12 * millisPerHour);
        tz.setEndRule(Calendar.OCTOBER, 20, 12 * millisPerHour);

        int offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.MARCH, 5,
                                  Calendar.THURSDAY, 10 * millisPerHour);
        if (offset != -5 * millisPerHour)
            errln("The offset for 10AM, 3/5/98 should have been -5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.MARCH, 15,
                              Calendar.SUNDAY, 10 * millisPerHour);
        if (offset != -4 * millisPerHour)
            errln("The offset for 10AM, 3/15/98 should have been -4 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.OCTOBER, 15,
                              Calendar.THURSDAY, 10 * millisPerHour);
        if (offset != -4 * millisPerHour)
            errln("The offset for 10AM, 10/15/98 should have been -4 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.OCTOBER, 25,
                              Calendar.SUNDAY, 10 * millisPerHour);
        if (offset != -5 * millisPerHour)
            errln("The offset for 10AM, 10/25/98 should have been -5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        // test the day-of-week-after-day-in-month API
        tz.setStartRule(Calendar.MARCH, 10, Calendar.FRIDAY, 12 * millisPerHour, true);
        tz.setEndRule(Calendar.OCTOBER, 20, Calendar.FRIDAY, 12 * millisPerHour, false);

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.MARCH, 11,
                              Calendar.WEDNESDAY, 10 * millisPerHour);
        if (offset != -5 * millisPerHour)
            errln("The offset for 10AM, 3/11/98 should have been -5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.MARCH, 14,
                              Calendar.SATURDAY, 10 * millisPerHour);
        if (offset != -4 * millisPerHour)
            errln("The offset for 10AM, 3/14/98 should have been -4 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.OCTOBER, 15,
                              Calendar.THURSDAY, 10 * millisPerHour);
        if (offset != -4 * millisPerHour)
            errln("The offset for 10AM, 10/15/98 should have been -4 hours, but we got "
                  + (offset / millisPerHour) + " hours.");

        offset = tz.getOffset(GregorianCalendar.AD, 1998, Calendar.OCTOBER, 17,
                              Calendar.SATURDAY, 10 * millisPerHour);
        if (offset != -5 * millisPerHour)
            errln("The offset for 10AM, 10/17/98 should have been -5 hours, but we got "
                  + (offset / millisPerHour) + " hours.");
    }

    public void TestEquivalencyGroups() {
        String id = "America/Los_Angeles";
        int n = TimeZone.countEquivalentIDs(id);
        if (n < 2) {
            errln("FAIL: countEquivalentIDs(" + id + ") returned " + n +
                  ", expected >= 2");
        }
        for (int i=0; i<n; ++i) {
            String s = TimeZone.getEquivalentID(id, i);
            if (s.length() == 0) {
                errln("FAIL: getEquivalentID(" + id + ", " + i +
                      ") returned \"" + s + "\", expected valid ID");
            } else {
                logln("" + i + ":" + s);
            }
        }

        // JB#5480 - equivalent IDs should not be empty within range
        String[] ids = TimeZone.getAvailableIDs();
        for (int i = 0; i < ids.length; i++) {
            int nEquiv = TimeZone.countEquivalentIDs(ids[i]);
            // Each equivalent ID must not be empty
            for (int j = 0; j < nEquiv; j++) {
                String equivID = TimeZone.getEquivalentID(ids[i], j);
                if (equivID.length() == 0) {
                    errln("FAIL: getEquivalentID(" + ids[i] + ", " + i +
                            ") returned \"" + equivID + "\", expected valid ID");
                }
            }
            // equivalent ID out of range must be empty
            String outOfRangeID = TimeZone.getEquivalentID(ids[i], nEquiv);
            if (outOfRangeID.length() != 0) {
                errln("FAIL: getEquivalentID(" + ids[i] + ", " + i +
                        ") returned \"" + outOfRangeID + "\", expected empty string");
            }
        }
    }

    public void TestCountries() {
        // Make sure America/Los_Angeles is in the "US" group, and
        // Asia/Tokyo isn't.  Vice versa for the "JP" group.

        String[] s = TimeZone.getAvailableIDs("US");
        boolean la = false, tokyo = false;
        String laZone = "America/Los_Angeles", tokyoZone = "Asia/Tokyo";

        for (int i=0; i<s.length; ++i) {
            if (s[i].equals(laZone)) {
                la = true;
            }
            if (s[i].equals(tokyoZone)) {
                tokyo = true;
            }
        }
        if (!la ) {
            errln("FAIL: " + laZone + " in US = " + la);
        }
        if (tokyo) {
            errln("FAIL: " + tokyoZone + " in US = " + tokyo);
        }
        s = TimeZone.getAvailableIDs("JP");
        la = false; tokyo = false;

        for (int i=0; i<s.length; ++i) {
            if (s[i].equals(laZone)) {
                la = true;
            }
            if (s[i].equals(tokyoZone)) {
                tokyo = true;
            }
        }
        if (la) {
            errln("FAIL: " + laZone + " in JP = " + la);
        }
        if (!tokyo) {
            errln("FAIL: " + tokyoZone + " in JP = " + tokyo);
        }
    }

    public void TestFractionalDST() {
        String tzName = "Australia/Lord_Howe"; // 30 min offset
        java.util.TimeZone tz_java = java.util.TimeZone.getTimeZone(tzName);
        int dst_java = 0;
        try {
            // hack so test compiles and runs in both JDK 1.3 and JDK 1.4
            final Object[] args = new Object[0];
            final Class[] argtypes = new Class[0];
            java.lang.reflect.Method m = tz_java.getClass().getMethod("getDSTSavings", argtypes); 
            dst_java = ((Integer) m.invoke(tz_java, args)).intValue();
            if (dst_java <= 0 || dst_java >= 3600000) { // didn't get the fractional time zone we wanted
            errln("didn't get fractional time zone!");
            }
        } catch (NoSuchMethodException e) {
            // see JDKTimeZone for the reason for this code
            dst_java = 3600000;
        } catch (IllegalAccessException e) {
            // see JDKTimeZone for the reason for this code
            errln(e.getMessage());
            dst_java = 3600000;
        } catch (InvocationTargetException e) {
            // see JDKTimeZone for the reason for this code
            errln(e.getMessage());
            dst_java = 3600000;
        } catch (SecurityException e) {
            warnln(e.getMessage());
            return;
        }
        
        com.ibm.icu.util.TimeZone tz_icu = com.ibm.icu.util.TimeZone.getTimeZone(tzName);
        int dst_icu = tz_icu.getDSTSavings();

        if (dst_java != dst_icu) {
            warnln("java reports dst savings of " + dst_java +
              " but icu reports " + dst_icu + 
              " for tz " + tz_icu.getID());
        } else {
            logln("both java and icu report dst savings of " + dst_java + " for tz " + tz_icu.getID());
        }
    }

    public void TestGetOffsetDate() {
        Calendar cal = Calendar.getInstance();
        cal.set(1997, Calendar.JANUARY, 30);
        long date = cal.getTimeInMillis();

    TimeZone tz_icu = TimeZone.getTimeZone("America/Los_Angeles");
    int offset = tz_icu.getOffset(date);
    if (offset != -28800000) {
        errln("expected offset -28800000, got: " + offset);
    }

    cal.set(1997, Calendar.JULY, 30);
    date = cal.getTimeInMillis();
    offset = tz_icu.getOffset(date);
    if (offset != -25200000) {
        errln("expected offset -25200000, got: " + offset);
    }
    }

    // jb4484
    public void TestSimpleTimeZoneSerialization() 
    {
        SimpleTimeZone stz0 = new SimpleTimeZone(32400000, "MyTimeZone");
        SimpleTimeZone stz1 = new SimpleTimeZone(32400000, "Asia/Tokyo");
        SimpleTimeZone stz2 = new SimpleTimeZone(32400000, "Asia/Tokyo");
        stz2.setRawOffset(0);
        SimpleTimeZone stz3 = new SimpleTimeZone(32400000, "Asia/Tokyo");
        stz3.setStartYear(100);
        SimpleTimeZone stz4 = new SimpleTimeZone(32400000, "Asia/Tokyo");
        stz4.setStartYear(1000);
        stz4.setDSTSavings(1800000);
        stz4.setStartRule(3, 4, 180000);
        stz4.setEndRule(6, 3, 4, 360000);
        SimpleTimeZone stz5 = new SimpleTimeZone(32400000, "Asia/Tokyo");
        stz5.setStartRule(2, 3, 4, 360000);
        stz5.setEndRule(6, 3, 4, 360000);
        
        SimpleTimeZone[] stzs = { stz0, stz1, stz2, stz3, stz4, stz5, };

        for (int i = 0; i < stzs.length; ++i) {
            SimpleTimeZone stz = stzs[i];
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(stz);
                oos.close();
                byte[] bytes = baos.toByteArray();
                logln("id: " + stz.getID() + " length: " + bytes.length);

                ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                ObjectInputStream ois = new ObjectInputStream(bais);

                SimpleTimeZone stzDeserialized = (SimpleTimeZone)ois.readObject();
                ois.close();

                assertEquals("time zones", stz, stzDeserialized);
            }
            catch (ClassCastException cce) {
                cce.printStackTrace();
                errln("could not deserialize SimpleTimeZone");
            }
            catch (IOException ioe) {
                errln(ioe.getMessage());
            }
            catch (ClassNotFoundException cnfe) {
                errln(cnfe.getMessage());
            }
        }
    }

    // jb4175
    /* Generated by org.unicode.cldr.tool.CountItems */
    private static final String[] timeZoneTestNames = {
        "America/Argentina/Buenos_Aires", "America/Buenos_Aires",
        "America/Argentina/Catamarca", "America/Catamarca",
        "America/Argentina/Cordoba", "America/Cordoba",
        "America/Argentina/Jujuy", "America/Jujuy",
        "America/Argentina/Mendoza", "America/Mendoza",
        "America/Atka", "America/Adak",
        "America/Ensenada", "America/Tijuana",
        "America/Fort_Wayne", "America/Indianapolis",
        "America/Indiana/Indianapolis", "America/Indianapolis",
        "America/Kentucky/Louisville", "America/Louisville",
        "America/Knox_IN", "America/Indiana/Knox",
        "America/Porto_Acre", "America/Rio_Branco",
        "America/Rosario", "America/Cordoba",
        "America/Virgin", "America/St_Thomas",
        "Asia/Ashkhabad", "Asia/Ashgabat",
        "Asia/Chungking", "Asia/Chongqing",
        "Asia/Dacca", "Asia/Dhaka",
        "Asia/Istanbul", "Europe/Istanbul",
        "Asia/Macao", "Asia/Macau",
        "Asia/Tel_Aviv", "Asia/Jerusalem",
        "Asia/Thimbu", "Asia/Thimphu",
        "Asia/Ujung_Pandang", "Asia/Makassar",
        "Asia/Ulan_Bator", "Asia/Ulaanbaatar",
        "Australia/ACT", "Australia/Sydney",
        "Australia/Canberra", "Australia/Sydney",
        "Australia/LHI", "Australia/Lord_Howe",
        "Australia/NSW", "Australia/Sydney",
        "Australia/North", "Australia/Darwin",
        "Australia/Queensland", "Australia/Brisbane",
        "Australia/South", "Australia/Adelaide",
        "Australia/Tasmania", "Australia/Hobart",
        "Australia/Victoria", "Australia/Melbourne",
        "Australia/West", "Australia/Perth",
        "Australia/Yancowinna", "Australia/Broken_Hill",
        "Brazil/Acre", "America/Rio_Branco",
        "Brazil/DeNoronha", "America/Noronha",
        "Brazil/East", "America/Sao_Paulo",
        "Brazil/West", "America/Manaus",
        "CST6CDT", "America/Chicago",
        "Canada/Atlantic", "America/Halifax",
        "Canada/Central", "America/Winnipeg",
        "Canada/East-Saskatchewan", "America/Regina",
        "Canada/Eastern", "America/Toronto",
        "Canada/Mountain", "America/Edmonton",
        "Canada/Newfoundland", "America/St_Johns",
        "Canada/Pacific", "America/Vancouver",
        "Canada/Saskatchewan", "America/Regina",
        "Canada/Yukon", "America/Whitehorse",
        "Chile/Continental", "America/Santiago",
        "Chile/EasterIsland", "Pacific/Easter",
        "Cuba", "America/Havana",
        "EST", "America/Indianapolis",
        "EST5EDT", "America/New_York",
        "Egypt", "Africa/Cairo",
        "Eire", "Europe/Dublin",
        "Etc/GMT+0", "Etc/GMT",
        "Etc/GMT-0", "Etc/GMT",
        "Etc/GMT0", "Etc/GMT",
        "Etc/Greenwich", "Etc/GMT",
        "Etc/UCT", "Etc/GMT",
        "Etc/UTC", "Etc/GMT",
        "Etc/Universal", "Etc/GMT",
        "Etc/Zulu", "Etc/GMT",
        "Europe/Nicosia", "Asia/Nicosia",
        "Europe/Tiraspol", "Europe/Chisinau",
        "GB", "Europe/London",
        "GB-Eire", "Europe/London",
        "GMT", "Etc/GMT",
        "GMT+0", "Etc/GMT",
        "GMT-0", "Etc/GMT",
        "GMT0", "Etc/GMT",
        "Greenwich", "Etc/GMT",
        "HST", "Pacific/Honolulu",
        "Hongkong", "Asia/Hong_Kong",
        "Iceland", "Atlantic/Reykjavik",
        "Iran", "Asia/Tehran",
        "Israel", "Asia/Jerusalem",
        "Jamaica", "America/Jamaica",
        "Japan", "Asia/Tokyo",
        "Kwajalein", "Pacific/Kwajalein",
        "Libya", "Africa/Tripoli",
        "MST", "America/Phoenix",
        "MST7MDT", "America/Denver",
        "Mexico/BajaNorte", "America/Tijuana",
        "Mexico/BajaSur", "America/Mazatlan",
        "Mexico/General", "America/Mexico_City",
        "NZ", "Pacific/Auckland",
        "NZ-CHAT", "Pacific/Chatham",
        "Navajo", "America/Shiprock", /* fixed from Mark's original */
        "PRC", "Asia/Shanghai",
        "PST8PDT", "America/Los_Angeles",
        "Pacific/Samoa", "Pacific/Pago_Pago",
        "Poland", "Europe/Warsaw",
        "Portugal", "Europe/Lisbon",
        "ROC", "Asia/Taipei",
        "ROK", "Asia/Seoul",
        "Singapore", "Asia/Singapore",
        "SystemV/AST4", "America/Puerto_Rico",
        "SystemV/AST4ADT", "America/Halifax",
        "SystemV/CST6", "America/Regina",
        "SystemV/CST6CDT", "America/Chicago",
        "SystemV/EST5", "America/Indianapolis",
        "SystemV/EST5EDT", "America/New_York",
        "SystemV/HST10", "Pacific/Honolulu",
        "SystemV/MST7", "America/Phoenix",
        "SystemV/MST7MDT", "America/Denver",
        "SystemV/PST8", "Pacific/Pitcairn",
        "SystemV/PST8PDT", "America/Los_Angeles",
        "SystemV/YST9", "Pacific/Gambier",
        "SystemV/YST9YDT", "America/Anchorage",
        "Turkey", "Europe/Istanbul",
        "UCT", "Etc/GMT",
        "US/Alaska", "America/Anchorage",
        "US/Aleutian", "America/Adak",
        "US/Arizona", "America/Phoenix",
        "US/Central", "America/Chicago",
        "US/East-Indiana", "America/Indianapolis",
        "US/Eastern", "America/New_York",
        "US/Hawaii", "Pacific/Honolulu",
        "US/Indiana-Starke", "America/Indiana/Knox",
        "US/Michigan", "America/Detroit",
        "US/Mountain", "America/Denver",
        "US/Pacific", "America/Los_Angeles",
        "US/Pacific-New", "America/Los_Angeles",
        "US/Samoa", "Pacific/Pago_Pago",
        "UTC", "Etc/GMT",
        "Universal", "Etc/GMT",
        "W-SU", "Europe/Moscow",
        "Zulu", "Etc/GMT",
    };

    public void TestOddTimeZoneNames() {
        for (int i = 0; i < timeZoneTestNames.length; i += 2) {
            String funkyName = timeZoneTestNames[i];
            String correctName = timeZoneTestNames[i+1];

            TimeZone ftz = TimeZone.getTimeZone(funkyName);
            TimeZone ctz = TimeZone.getTimeZone(correctName);

            String fdn = ftz.getDisplayName();
            long fro = ftz.getRawOffset();
            long fds = ftz.getDSTSavings();
            boolean fdy = ftz.useDaylightTime();

            String cdn = ctz.getDisplayName();
            long cro = ctz.getRawOffset();
            long cds = ctz.getDSTSavings();
            boolean cdy = ctz.useDaylightTime();

            if (!fdn.equals(cdn)) {
                logln("display name (" + funkyName + ", " + correctName + ") expected: " + cdn + " but got: " + fdn);
            } else if (fro != cro) {
                logln("offset (" + funkyName + ", " + correctName + ") expected: " + cro + " but got: " + fro);
            } else if (fds != cds) {
                logln("daylight (" + funkyName + ", " + correctName + ") expected: " + cds + " but got: " + fds);
            } else if (fdy != cdy) {
                logln("uses daylight (" + funkyName + ", " + correctName + ") expected: " + cdy + " but got: " + fdy);
            } else {
                // no error, assume we're referencing the same internal java object
            }
        }
    }
    
    public void TestCoverage(){
        class StubTimeZone extends TimeZone{
            /**
             * For serialization
             */
            private static final long serialVersionUID = 8658654217433379343L;
            public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) {return 0;}
            public void setRawOffset(int offsetMillis) {}
            public int getRawOffset() {return 0;}
            public boolean useDaylightTime() {return false;}
            public boolean inDaylightTime(Date date) {return false;}
        } 
        StubTimeZone stub = new StubTimeZone();
        StubTimeZone stub2 = (StubTimeZone) stub.clone();
        if (stub.getDSTSavings() != 0){
            errln("TimeZone.getDSTSavings() should return 0");
        }
        if (!stub.hasSameRules(stub2)){
            errln("TimeZone.clone() object should hasSameRules");
     
        }
    }
    public void TestMark(){
        String tzid = "America/Argentina/ComodRivadavia";
        TimeZone tz = TimeZone.getTimeZone(tzid);
        int offset = tz.getOffset(new Date().getTime());
        logln(tzid + ":\t" + offset);
        List list = Arrays.asList(TimeZone.getAvailableIDs());
        if(!list.contains(tzid)){
            errln("Could create the time zone but it is not in getAvailableIDs");
        }
    }
    public void TestZoneMeta() {
        java.util.TimeZone save = java.util.TimeZone.getDefault();
        java.util.TimeZone newZone = java.util.TimeZone.getTimeZone("GMT-08:00");
        com.ibm.icu.util.TimeZone.setDefault(null);
        java.util.TimeZone.setDefault(newZone);
        SimpleTimeZone zone = new SimpleTimeZone(0, "GMT");
        com.ibm.icu.util.TimeZone defaultZone = com.ibm.icu.util.TimeZone.getDefault();
        if(defaultZone==null){
            errln("TimeZone.getDefault() failed for GMT-08:00");
        }
        if(zone==null){
            errln("SimpleTimeZone(0, GMT-08:00) failed for GMT-08:00");
        }
        //reset
        java.util.TimeZone.setDefault(save);
    }

    // Copied from the protected constant in TimeZone.
    private static final int MILLIS_PER_HOUR = 60*60*1000;

    //  Test that a transition at the end of February is handled correctly.
    public void TestFebruary() {
        // Time zone with daylight savings time from the first Sunday in November
        // to the last Sunday in February.
        // Similar to the new rule for Brazil (Sao Paulo) in tzdata2006n.
        //
        // Note: In tzdata2007h, the rule had changed, so no actual zones uses
        // lastSun in Feb anymore.
        SimpleTimeZone tz1 = new SimpleTimeZone(
                           -3 * MILLIS_PER_HOUR,                    // raw offset: 3h before (west of) GMT
                           "nov-feb",
                           Calendar.NOVEMBER, 1, Calendar.SUNDAY,   // start: November, first, Sunday
                           0,                                       //        midnight wall time
                           Calendar.FEBRUARY, -1, Calendar.SUNDAY,  // end:   February, last, Sunday
                           0);                                      //        midnight wall time

        // Now hardcode the same rules as for Brazil in tzdata 2006n, so that
        // we cover the intended code even when in the future zoneinfo hardcodes
        // these transition dates.
        SimpleTimeZone tz2= new SimpleTimeZone(
                           -3 * MILLIS_PER_HOUR,                    // raw offset: 3h before (west of) GMT
                           "nov-feb2",
                           Calendar.NOVEMBER, 1, -Calendar.SUNDAY,  // start: November, 1 or after, Sunday
                           0,                                       //        midnight wall time
                           Calendar.FEBRUARY, -29, -Calendar.SUNDAY,// end:   February, 29 or before, Sunday
                           0);                                      //        midnight wall time

        // Gregorian calendar with the UTC time zone for getting sample test date/times.
        GregorianCalendar gc = new GregorianCalendar(TimeZone.getTimeZone("Etc/GMT"));
        // "Unable to create the UTC calendar: %s"

        int[] data = {
            // UTC time (6 fields) followed by
            // expected time zone offset in hours after GMT (negative=before GMT).
            // int year, month, day, hour, minute, second, offsetHours
            2006, Calendar.NOVEMBER,  5, 02, 59, 59, -3,
            2006, Calendar.NOVEMBER,  5, 03, 00, 00, -2,
            2007, Calendar.FEBRUARY, 25, 01, 59, 59, -2,
            2007, Calendar.FEBRUARY, 25, 02, 00, 00, -3,

            2007, Calendar.NOVEMBER,  4, 02, 59, 59, -3,
            2007, Calendar.NOVEMBER,  4, 03, 00, 00, -2,
            2008, Calendar.FEBRUARY, 24, 01, 59, 59, -2,
            2008, Calendar.FEBRUARY, 24, 02, 00, 00, -3,

            2008, Calendar.NOVEMBER,  2, 02, 59, 59, -3,
            2008, Calendar.NOVEMBER,  2, 03, 00, 00, -2,
            2009, Calendar.FEBRUARY, 22, 01, 59, 59, -2,
            2009, Calendar.FEBRUARY, 22, 02, 00, 00, -3,

            2009, Calendar.NOVEMBER,  1, 02, 59, 59, -3,
            2009, Calendar.NOVEMBER,  1, 03, 00, 00, -2,
            2010, Calendar.FEBRUARY, 28, 01, 59, 59, -2,
            2010, Calendar.FEBRUARY, 28, 02, 00, 00, -3
        };

        TimeZone timezones[] = { tz1, tz2 };

        TimeZone tz;
        Date dt;
        int t, i, raw, dst;
        int[] offsets = new int[2]; // raw = offsets[0], dst = offsets[1]
        for (t = 0; t < timezones.length; ++t) {
            tz = timezones[t];
            for (i = 0; i < data.length; i+=7) {
                gc.set(data[i], data[i+1], data[i+2],
                       data[i+3], data[i+4], data[i+5]);
                dt = gc.getTime();
                tz.getOffset(dt.getTime(), false, offsets);
                raw = offsets[0];
                dst = offsets[1];
                if ((raw + dst) != data[i+6] * MILLIS_PER_HOUR) {
                    errln("test case " + t + "." + (i/7) + ": " +
                          "tz.getOffset(" + data[i] + "-" + (data[i+1] + 1) + "-" + data[i+2] + " " +
                          data[i+3] + ":" + data[i+4] + ":" + data[i+5] +
                          ") returns " + raw + "+" + dst + " != " + data[i+6] * MILLIS_PER_HOUR);
                }
            }
        }
    }

    public void TestCanonicalID() {
        // Some canonical IDs in CLDR are defined as "Link"
        // in Olson tzdata.
        final String[][] excluded1 = {
                {"America/Shiprock", "America/Denver"}, // America/Shiprock is defined as a Link to America/Denver in tzdata
                {"America/Marigot", "America/Guadeloupe"},
                {"America/St_Barthelemy", "America/Guadeloupe"},
                {"Antarctica/South_Pole", "Antarctica/McMurdo"},
                {"Atlantic/Jan_Mayen", "Europe/Oslo"},
                {"Arctic/Longyearbyen", "Europe/Oslo"},
                {"Europe/Guernsey", "Europe/London"},
                {"Europe/Isle_of_Man", "Europe/London"},
                {"Europe/Jersey", "Europe/London"},
                {"Europe/Ljubljana", "Europe/Belgrade"},
                {"Europe/Podgorica", "Europe/Belgrade"},
                {"Europe/Sarajevo", "Europe/Belgrade"},
                {"Europe/Skopje", "Europe/Belgrade"},
                {"Europe/Zagreb", "Europe/Belgrade"},
                {"Europe/Bratislava", "Europe/Prague"},
                {"Europe/Mariehamn", "Europe/Helsinki"},
                {"Europe/San_Marino", "Europe/Rome"},
                {"Europe/Vatican", "Europe/Rome"},
        };

        // Following IDs are aliases of Etc/GMT in CLDR,
        // but Olson tzdata has 3 independent definitions
        // for Etc/GMT, Etc/UTC, Etc/UCT.
        // Until we merge them into one equivalent group
        // in zoneinfo.res, we exclude them in the test
        // below.
        final String[] excluded2 = {
                "Etc/UCT", "UCT",
                "Etc/UTC", "UTC",
                "Etc/Universal", "Universal",
                "Etc/Zulu", "Zulu",
        };

        // Walk through equivalency groups
        String[] ids = TimeZone.getAvailableIDs();
        for (int i = 0; i < ids.length; i++) {
            int nEquiv = TimeZone.countEquivalentIDs(ids[i]);
            if (nEquiv == 0) {
                continue;
            }
            String canonicalID = null;
            boolean bFoundCanonical = false;
            // Make sure getCanonicalID returns the exact same result
            // for all entries within a same equivalency group with some
            // exceptions listed in exluded1.
            // Also, one of them must be canonical id.
            for (int j = 0; j < nEquiv; j++) {
                String tmp = TimeZone.getEquivalentID(ids[i], j);
                String tmpCanonical = TimeZone.getCanonicalID(tmp);
                if (tmpCanonical == null) {
                    errln("FAIL: getCanonicalID(\"" + tmp + "\") returned null");
                    continue;
                }
                // Some exceptional cases
                for (int k = 0; k < excluded1.length; k++) {
                    if (tmpCanonical.equals(excluded1[k][0])) {
                        tmpCanonical = excluded1[k][1];
                    }
                }

                if (j == 0) {
                    canonicalID = tmpCanonical;
                } else if (!canonicalID.equals(tmpCanonical)) {
                    errln("FAIL: getCanonicalID(\"" + tmp + "\") returned " + tmpCanonical + " expected:" + canonicalID);
                }

                if (canonicalID.equals(tmp)) {
                    bFoundCanonical = true;
                }
            }
            // At least one ID in an equvalency group must match the
            // canonicalID
            if (!bFoundCanonical) {
                // test exclusion because of differences between Olson tzdata and CLDR
                boolean isExcluded = false;
                for (int k = 0; k < excluded1.length; k++) {
                    if (ids[i].equals(excluded2[k])) {
                        isExcluded = true;
                        break;
                    }
                }
                if (isExcluded) {
                    continue;
                }

                errln("FAIL: No timezone ids match the canonical ID " + canonicalID);
            }
        }
        // Testing some special cases
        final String[][] data = {
                {"GMT-03", "GMT-03:00", null},
                {"GMT+4", "GMT+04:00", null},
                {"GMT-055", "GMT-00:55", null},
                {"GMT+430", "GMT+04:30", null},
                {"GMT-12:15", "GMT-12:15", null},
                {"GMT-091015", "GMT-09:10:15", null},
                {"GMT+1:90", null, null},
                {"America/Argentina/Buenos_Aires", "America/Buenos_Aires", "true"},
                {"bogus", null, null},
                {"", null, null},
                {null, null, null},
        };
        boolean[] isSystemID = new boolean[1];
        for (int i = 0; i < data.length; i++) {
            String canonical = TimeZone.getCanonicalID(data[i][0], isSystemID);
            if (canonical != null && !canonical.equals(data[i][1])
                    || canonical == null && data[i][1] != null) {
                errln("FAIL: getCanonicalID(\"" + data[i][0] + "\") returned " + canonical
                        + " - expected: " + data[i][1]);
            }
            if ("true".equalsIgnoreCase(data[i][2]) != isSystemID[0]) {
                errln("FAIL: getCanonicalID(\"" + data[i][0] + "\") set " + isSystemID[0]
                        + " to isSystemID");
            }
        }
    }

    public void TestSetDefault() {
        java.util.TimeZone save = java.util.TimeZone.getDefault();

        /*
         * America/Caracs (Venezuela) changed the base offset from -4:00 to
         * -4:30 on Dec 9, 2007.
         */

        TimeZone icuCaracas = TimeZone.getTimeZone("America/Caracas", TimeZone.TIMEZONE_ICU);
        java.util.TimeZone jdkCaracas = java.util.TimeZone.getTimeZone("America/Caracas");

        // Set JDK America/Caracas as the default
        java.util.TimeZone.setDefault(jdkCaracas);

        java.util.Calendar jdkCal = java.util.Calendar.getInstance();
        jdkCal.clear();
        jdkCal.set(2007, java.util.Calendar.JANUARY, 1);

        int rawOffset = jdkCal.get(java.util.Calendar.ZONE_OFFSET);
        int dstSavings = jdkCal.get(java.util.Calendar.DST_OFFSET);

        int[] offsets = new int[2];
        icuCaracas.getOffset(jdkCal.getTime().getTime()/*jdkCal.getTimeInMillis()*/, false, offsets);

        boolean isTimeZoneSynchronized = true;

        if (rawOffset != offsets[0] || dstSavings != offsets[1]) {
            // JDK time zone rule is out of sync...
            logln("Rule for JDK America/Caracas is not same with ICU.  Skipping the rest.");
            isTimeZoneSynchronized = false;
        }

        if (isTimeZoneSynchronized) {
            // If JDK America/Caracas uses the same rule with ICU,
            // the following code should work well.
            TimeZone.setDefault(icuCaracas);

            // Create a new JDK calendar instance again.
            // This calendar should reflect the new default
            // set by ICU TimeZone#setDefault.
            jdkCal = java.util.Calendar.getInstance();
            jdkCal.clear();
            jdkCal.set(2007, java.util.Calendar.JANUARY, 1);

            rawOffset = jdkCal.get(java.util.Calendar.ZONE_OFFSET);
            dstSavings = jdkCal.get(java.util.Calendar.DST_OFFSET);

            if (rawOffset != offsets[0] || dstSavings != offsets[1]) {
                errln("ERROR: Got offset [raw:" + rawOffset + "/dst:" + dstSavings
                          + "] Expected [raw:" + offsets[0] + "/dst:" + offsets[1] + "]");
            }
        }

        // Restore the original JDK time zone
        java.util.TimeZone.setDefault(save);
    }

    /*
     * Test Display Names, choosing zones and lcoales where there are multiple
     * meta-zones defined.
     */
    public void TestDisplayNamesMeta() {
        final Integer TZSHORT = new Integer(TimeZone.SHORT);
        final Integer TZLONG = new Integer(TimeZone.LONG);

        final Object[][] zoneDisplayTestData = {
            //  zone id             locale  summer          format      expected display name
            {"Europe/London",       "en",   Boolean.FALSE,  TZSHORT,    "GMT"},
            {"Europe/London",       "en",   Boolean.FALSE,  TZLONG,     "Greenwich Mean Time"},
            {"Europe/London",       "en",   Boolean.TRUE,   TZSHORT,    "GMT+01:00" /*"BST"*/},
            {"Europe/London",       "en",   Boolean.TRUE,   TZLONG,     "British Summer Time"},

            {"America/Anchorage",   "en",   Boolean.FALSE,  TZSHORT,    "AKST"},
            {"America/Anchorage",   "en",   Boolean.FALSE,  TZLONG,     "Alaska Standard Time"},
            {"America/Anchorage",   "en",   Boolean.TRUE,   TZSHORT,    "AKDT"},
            {"America/Anchorage",   "en",   Boolean.TRUE,   TZLONG,     "Alaska Daylight Time"},

            // Southern Hemisphere, all data from meta:Australia_Western
            {"Australia/Perth",     "en",   Boolean.FALSE,  TZSHORT,    "GMT+08:00"/*"AWST"*/},
            {"Australia/Perth",     "en",   Boolean.FALSE,  TZLONG,     "Australian Western Standard Time"},
            {"Australia/Perth",     "en",   Boolean.TRUE,   TZSHORT,    "GMT+09:00"/*"AWDT"*/},
            {"Australia/Perth",     "en",   Boolean.TRUE,   TZLONG,     "Australian Western Daylight Time"},

            {"America/Sao_Paulo",   "en",   Boolean.FALSE,  TZSHORT,    "GMT-03:00"/*"BRT"*/},
            {"America/Sao_Paulo",   "en",   Boolean.FALSE,  TZLONG,     "Brasilia Time"},
            {"America/Sao_Paulo",   "en",   Boolean.TRUE,   TZSHORT,    "GMT-02:00"/*"BRST"*/},
            {"America/Sao_Paulo",   "en",   Boolean.TRUE,   TZLONG,     "Brasilia Summer Time"},

            // No Summer Time, but had it before 1983.
            {"Pacific/Honolulu",    "en",   Boolean.FALSE,  TZSHORT,    "HST"},
            {"Pacific/Honolulu",    "en",   Boolean.FALSE,  TZLONG,     "Hawaii-Aleutian Standard Time"},
            {"Pacific/Honolulu",    "en",   Boolean.TRUE,   TZSHORT,    "HST"},
            {"Pacific/Honolulu",    "en",   Boolean.TRUE,   TZLONG,     "Hawaii-Aleutian Standard Time"},

            // Northern, has Summer, not commonly used.
            {"Europe/Helsinki",     "en",   Boolean.FALSE,  TZSHORT,    "GMT+02:00"/*"EET"*/},
            {"Europe/Helsinki",     "en",   Boolean.FALSE,  TZLONG,     "Eastern European Time"},
            {"Europe/Helsinki",     "en",   Boolean.TRUE,   TZSHORT,    "GMT+03:00"/*"EEST"*/},
            {"Europe/Helsinki",     "en",   Boolean.TRUE,   TZLONG,     "Eastern European Summer Time"},

            // Repeating the test data for DST.  The test data below trigger the problem reported
            // by Ticket#6644
            {"Europe/London",       "en",   Boolean.TRUE,   TZSHORT,    "GMT+01:00" /*"BST"*/},
            {"Europe/London",       "en",   Boolean.TRUE,   TZLONG,     "British Summer Time"},
        };

        boolean isReferenceYear = true;
        GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/GMT"));
        if (cal.get(Calendar.YEAR) != REFERENCE_YEAR) {
            isReferenceYear = false;
        }
        boolean isICUTimeZone = (TimeZone.getDefaultTimeZoneType() == TimeZone.TIMEZONE_ICU);

        boolean sawAnError = false;
        for (int testNum = 0; testNum < zoneDisplayTestData.length; testNum++) {
            ULocale locale = new ULocale((String)zoneDisplayTestData[testNum][1]);
            TimeZone zone = TimeZone.getTimeZone((String)zoneDisplayTestData[testNum][0]);
            String displayName = zone.getDisplayName(((Boolean)zoneDisplayTestData[testNum][2]).booleanValue(),
                    ((Integer)zoneDisplayTestData[testNum][3]).intValue());
            if (!displayName.equals(zoneDisplayTestData[testNum][4])) {
                if (isReferenceYear
                        && (isICUTimeZone || !((Boolean)zoneDisplayTestData[testNum][2]).booleanValue())) {
                    sawAnError = true;
                    errln("Incorrect time zone display name.  zone = "
                            + zoneDisplayTestData[testNum][0] + ",\n"
                            + "   locale = " + locale
                            + ",   style = " + (zoneDisplayTestData[testNum][3] == TZSHORT ? "SHORT" : "LONG")
                            + ",   Summertime = " + zoneDisplayTestData[testNum][2] + "\n"
                            + "   Expected " + zoneDisplayTestData[testNum][4]
                            + ",   Got " + displayName);
                } else {
                    logln("Incorrect time zone display name.  zone = "
                            + zoneDisplayTestData[testNum][0] + ",\n"
                            + "   locale = " + locale
                            + ",   style = " + (zoneDisplayTestData[testNum][3] == TZSHORT ? "SHORT" : "LONG")
                            + ",   Summertime = " + zoneDisplayTestData[testNum][2] + "\n"
                            + "   Expected " + zoneDisplayTestData[testNum][4]
                            + ",   Got " + displayName);
                }
            }
        }
        if (sawAnError) {
            logln("Note: Errors could be the result of changes to zoneStrings locale data");
        }
    }

    /*
     * Test case for hashCode problem reported by ticket#7690 OlsonTimeZone.hashCode() throws NPE.
     */
    public void TestHashCode() {
        String[] ids = TimeZone.getAvailableIDs();

        for (String id: ids) {
            TimeZone tz1 = TimeZone.getTimeZone(id);
            TimeZone tz2 = TimeZone.getTimeZone(id);

            // hash code are same for the same time zone
            if (tz1.hashCode() != tz2.hashCode()) {
                errln("Fail: Two time zone instances for " + id + " have different hash values.");
            }
            // string representation should be also same
            if (!tz1.toString().equals(tz2.toString())) {
                errln("Fail: Two time zone instances for " + id + " have different toString() values.");
            }
        }
    }
}

//eof
