BasicTimeZone.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2007-2014, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.util;

import com.ibm.icu.impl.Grego;
import java.util.BitSet;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

/**
 * {@icu} BasicTimeZone extends <code>TimeZone</code> with additional methods to access time zone
 * transitions and rules. All ICU <code>TimeZone</code> concrete subclasses extend this class. APIs
 * added to <code>java.util.TimeZone</code> by <code>BasicTimeZone</code> are annotated with
 * <strong>'<span style="color:red">[icu]</span>'</strong>.
 *
 * @see com.ibm.icu.util.TimeZoneRule
 * @see com.ibm.icu.util.TimeZoneTransition
 * @stable ICU 3.8
 */
public abstract class BasicTimeZone extends TimeZone {

    private static final long serialVersionUID = -3204278532246180932L;

    private static final long MILLIS_PER_YEAR = 365 * 24 * 60 * 60 * 1000L;

    /**
     * {@icu} Returns the first time zone transition after the base time.
     * <!-- From: com.ibm.icu.samples.util.timezone.BasicTimeZoneExample:getNextTransitionExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * System.out.println("### Iterates time zone transitions in America/Los_Angeles starting 2005-01-01 and forward");
     *
     * // A TimeZone instance created by getTimeZone with TIMEZONE_ICU is always a BasicTimeZone
     * BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone("America/Los_Angeles", TimeZone.TIMEZONE_ICU);
     *
     * // Date format for the wall time
     * SimpleDateFormat wallTimeFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", ULocale.US);
     * wallTimeFmt.setTimeZone(btz);
     *
     * long start = 1104537600000L;    // 2005-01-01 0:00 UTC
     * for (int i = 0; i &lt; 5; i++) {   // Up to 5 transitions
     *     TimeZoneTransition trans = btz.getNextTransition(start, false /* not including start time *<code>/</code>);
     *
     *     // Display the transition time and offset information
     *     long transTime = trans.getTime();
     *     System.out.println(wallTimeFmt.format(new Date(transTime - 1)) + " -&gt; " + wallTimeFmt.format(new Date(transTime)));
     *     System.out.println(" - Before (Offset/Save): " + trans.getFrom().getRawOffset() + "/" + trans.getFrom().getDSTSavings());
     *     System.out.println(" - After  (Offset/Save): " + trans.getTo().getRawOffset() + "/" + trans.getTo().getDSTSavings());
     *
     *     // Update start time for next transition
     *     start = transTime;
     * }
     * </pre>
     *
     * @param base The base time.
     * @param inclusive Whether the base time is inclusive or not.
     * @return A <code>Date</code> holding the first time zone transition time after the given base
     *     time, or null if no time zone transitions are available after the base time.
     * @stable ICU 3.8
     */
    public abstract TimeZoneTransition getNextTransition(long base, boolean inclusive);

    /**
     * {@icu} Returns the last time zone transition before the base time.
     * <!-- From: com.ibm.icu.samples.util.timezone.BasicTimeZoneExample:getPreviousTransitionExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * System.out.println("### Iterates time zone transitions in America/Los_Angeles starting 2010-01-01 and backward");
     *
     * // A TimeZone instance created by getTimeZone with TIMEZONE_ICU is always a BasicTimeZone
     * BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone("America/Los_Angeles", TimeZone.TIMEZONE_ICU);
     *
     * // Date format for the wall time
     * SimpleDateFormat wallTimeFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", ULocale.US);
     * wallTimeFmt.setTimeZone(btz);
     *
     * long start = 1262304000000L;    // 2010-01-01 0:00 UTC
     * for (int i = 0; i &lt; 5; i++) {   // Up to 5 transitions
     *     TimeZoneTransition trans = btz.getPreviousTransition(start, false /* not including start time *<code>/</code>);
     *
     *     // Display the transition time and offset information
     *     long transTime = trans.getTime();
     *     System.out.println(wallTimeFmt.format(new Date(transTime - 1)) + " -&gt; " + wallTimeFmt.format(new Date(transTime)));
     *     System.out.println(" - Before (Offset/Save): " + trans.getFrom().getRawOffset() + "/" + trans.getFrom().getDSTSavings());
     *     System.out.println(" - After  (Offset/Save): " + trans.getTo().getRawOffset() + "/" + trans.getTo().getDSTSavings());
     *
     *     // Update start time for next transition
     *     start = transTime;
     * }
     * </pre>
     *
     * @param base The base time.
     * @param inclusive Whether the base time is inclusive or not.
     * @return A <code>Date</code> holding the last time zone transition time before the given base
     *     time, or null if no time zone transitions are available before the base time.
     * @stable ICU 3.8
     */
    public abstract TimeZoneTransition getPreviousTransition(long base, boolean inclusive);

    /**
     * {@icu} Checks if the time zone has equivalent transitions in the time range. This method
     * returns true when all of transition times, from/to standard offsets and DST savings used by
     * this time zone match the other in the time range.
     * <!-- From: com.ibm.icu.samples.util.timezone.BasicTimeZoneExample:hasEquivalentTransitionsExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * System.out.println("### Compare America/New_York and America/Detroit since year 1970");
     *
     * // A TimeZone instance created by getTimeZone with TIMEZONE_ICU is always a BasicTimeZone
     * BasicTimeZone tzNewYork = (BasicTimeZone)TimeZone.getTimeZone("America/New_York", TimeZone.TIMEZONE_ICU);
     * BasicTimeZone tzDetroit = (BasicTimeZone)TimeZone.getTimeZone("America/Detroit", TimeZone.TIMEZONE_ICU);
     *
     * GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("Etc/GMT"));
     *
     * // Compare these time zones every 10 years since year 1970 up to year 2009
     * for (int startYear = 1970; startYear &lt;= 2000; startYear += 10) {
     *     long start, end;
     *
     *     cal.set(startYear, Calendar.JANUARY, 1, 0, 0, 0);
     *     cal.set(Calendar.MILLISECOND, 0);
     *     start = cal.getTimeInMillis();
     *
     *     // Set the end time to the end of startYear + 9
     *     int endYear = startYear + 9;
     *     cal.set(endYear + 1, Calendar.JANUARY, 1, 0, 0, 0);
     *     end = cal.getTimeInMillis() - 1;
     *
     *     // Check if these two zones have equivalent time zone transitions for the given time range
     *     boolean isEquivalent = tzNewYork.hasEquivalentTransitions(tzDetroit, start, end);
     *     System.out.println(startYear + "-" + endYear + ": " + isEquivalent);
     * }
     * </pre>
     *
     * @param tz The instance of <code>TimeZone</code>
     * @param start The start time of the evaluated time range (inclusive)
     * @param end The end time of the evaluated time range (inclusive)
     * @return true if the other time zone has the equivalent transitions in the time range. When tz
     *     is not a <code>BasicTimeZone</code>, this method returns false.
     * @stable ICU 3.8
     */
    public boolean hasEquivalentTransitions(TimeZone tz, long start, long end) {
        return hasEquivalentTransitions(tz, start, end, false);
    }

    /**
     * {@icu} Checks if the time zone has equivalent transitions in the time range. This method
     * returns true when all of transition times, from/to standard offsets and DST savings used by
     * this time zone match the other in the time range.
     *
     * @param tz The instance of <code>TimeZone</code>
     * @param start The start time of the evaluated time range (inclusive)
     * @param end The end time of the evaluated time range (inclusive)
     * @param ignoreDstAmount When true, any transitions with only daylight saving amount changes
     *     will be ignored, except either of them is zero. For example, a transition from rawoffset
     *     3:00/dstsavings 1:00 to rawoffset 2:00/dstsavings 2:00 is excluded from the comparison,
     *     but a transition from rawoffset 2:00/dstsavings 1:00 to rawoffset 3:00/dstsavings 0:00 is
     *     included.
     * @return true if the other time zone has the equivalent transitions in the time range. When tz
     *     is not a <code>BasicTimeZone</code>, this method returns false.
     * @stable ICU 3.8
     */
    public boolean hasEquivalentTransitions(
            TimeZone tz, long start, long end, boolean ignoreDstAmount) {
        if (this == tz) {
            return true;
        }

        if (!(tz instanceof BasicTimeZone)) {
            return false;
        }

        // Check the offsets at the start time
        int[] offsets1 = new int[2];
        int[] offsets2 = new int[2];

        getOffset(start, false, offsets1);
        tz.getOffset(start, false, offsets2);

        if (ignoreDstAmount) {
            if ((offsets1[0] + offsets1[1] != offsets2[0] + offsets2[1])
                    || (offsets1[1] != 0 && offsets2[1] == 0)
                    || (offsets1[1] == 0 && offsets2[1] != 0)) {
                return false;
            }
        } else {
            if (offsets1[0] != offsets2[0] || offsets1[1] != offsets2[1]) {
                return false;
            }
        }

        // Check transitions in the range
        long time = start;
        while (true) {
            TimeZoneTransition tr1 = getNextTransition(time, false);
            TimeZoneTransition tr2 = ((BasicTimeZone) tz).getNextTransition(time, false);

            if (ignoreDstAmount) {
                // Skip a transition which only differ the amount of DST savings
                while (true) {
                    if (tr1 != null
                            && tr1.getTime() <= end
                            && (tr1.getFrom().getRawOffset() + tr1.getFrom().getDSTSavings()
                                    == tr1.getTo().getRawOffset() + tr1.getTo().getDSTSavings())
                            && (tr1.getFrom().getDSTSavings() != 0
                                    && tr1.getTo().getDSTSavings() != 0)) {
                        tr1 = getNextTransition(tr1.getTime(), false);
                    } else {
                        break;
                    }
                }
                while (true) {
                    if (tr2 != null
                            && tr2.getTime() <= end
                            && (tr2.getFrom().getRawOffset() + tr2.getFrom().getDSTSavings()
                                    == tr2.getTo().getRawOffset() + tr2.getTo().getDSTSavings())
                            && (tr2.getFrom().getDSTSavings() != 0
                                    && tr2.getTo().getDSTSavings() != 0)) {
                        tr2 = ((BasicTimeZone) tz).getNextTransition(tr2.getTime(), false);
                    } else {
                        break;
                    }
                }
            }

            boolean inRange1 = false;
            boolean inRange2 = false;
            if (tr1 != null) {
                if (tr1.getTime() <= end) {
                    inRange1 = true;
                }
            }
            if (tr2 != null) {
                if (tr2.getTime() <= end) {
                    inRange2 = true;
                }
            }
            if (!inRange1 && !inRange2) {
                // No more transition in the range
                break;
            }
            if (!inRange1 || !inRange2) {
                return false;
            }
            if (tr1.getTime() != tr2.getTime()) {
                return false;
            }
            if (ignoreDstAmount) {
                if (tr1.getTo().getRawOffset() + tr1.getTo().getDSTSavings()
                                != tr2.getTo().getRawOffset() + tr2.getTo().getDSTSavings()
                        || tr1.getTo().getDSTSavings() != 0 && tr2.getTo().getDSTSavings() == 0
                        || tr1.getTo().getDSTSavings() == 0 && tr2.getTo().getDSTSavings() != 0) {
                    return false;
                }
            } else {
                if (tr1.getTo().getRawOffset() != tr2.getTo().getRawOffset()
                        || tr1.getTo().getDSTSavings() != tr2.getTo().getDSTSavings()) {
                    return false;
                }
            }
            time = tr1.getTime();
        }
        return true;
    }

    /**
     * {@icu} Returns the array of <code>TimeZoneRule</code> which represents the rule of this time
     * zone object. The first element in the result array will be the <code>InitialTimeZoneRule
     * </code> instance for the initial rule. The rest will be either <code>AnnualTimeZoneRule
     * </code> or <code>TimeArrayTimeZoneRule</code> instances representing transitions.
     *
     * @return The array of <code>TimeZoneRule</code> which represents this time zone.
     * @stable ICU 3.8
     */
    public abstract TimeZoneRule[] getTimeZoneRules();

    /**
     * {@icu} Returns the array of <code>TimeZoneRule</code> which represents the rule of this time
     * zone object since the specified start time. The first element in the result array will be the
     * <code>InitialTimeZoneRule</code> instance for the initial rule. The rest will be either
     * <code>AnnualTimeZoneRule</code> or <code>TimeArrayTimeZoneRule</code> instances representing
     * transitions.
     * <!-- From: com.ibm.icu.samples.util.timezone.BasicTimeZoneExample:getTimeZoneRulesExample -->
     *
     * <p>Example code:
     *
     * <pre>
     * System.out.println("### Extracts time zone rules used by America/Los_Angeles since year 2005");
     *
     * // A TimeZone instance created by getTimeZone with TIMEZONE_ICU is always a BasicTimeZone
     * BasicTimeZone btz = (BasicTimeZone)TimeZone.getTimeZone("America/Los_Angeles", TimeZone.TIMEZONE_ICU);
     * long since = 1104537600000L;    // 2005-01-01 0:00 UTC
     * TimeZoneRule[] rules = btz.getTimeZoneRules(since);
     * System.out.println("Rule(initial): " + rules[0]);
     * for (int i = 1; i &lt; rules.length; i++) {
     *     System.out.println("Rule: " + rules[i]);
     * }
     * </pre>
     *
     * @param start The start time (inclusive).
     * @return The array of <code>TimeZoneRule</code> which represents this time zone since the
     *     start time.
     * @stable ICU 3.8
     */
    public TimeZoneRule[] getTimeZoneRules(long start) {
        TimeZoneRule[] all = getTimeZoneRules();
        TimeZoneTransition tzt = getPreviousTransition(start, true);
        if (tzt == null) {
            // No need to filter out rules only applicable to time before the start
            return all;
        }

        BitSet isProcessed = new BitSet(all.length);
        List<TimeZoneRule> filteredRules = new LinkedList<>();

        // Create initial rule
        TimeZoneRule initial =
                new InitialTimeZoneRule(
                        tzt.getTo().getName(),
                        tzt.getTo().getRawOffset(),
                        tzt.getTo().getDSTSavings());
        filteredRules.add(initial);
        isProcessed.set(0);

        // Mark rules which does not need to be processed
        for (int i = 1; i < all.length; i++) {
            Date d =
                    all[i].getNextStart(
                            start, initial.getRawOffset(), initial.getDSTSavings(), false);
            if (d == null) {
                isProcessed.set(i);
            }
        }

        long time = start;
        boolean bFinalStd = false, bFinalDst = false;
        while (!bFinalStd || !bFinalDst) {
            tzt = getNextTransition(time, false);
            if (tzt == null) {
                break;
            }
            time = tzt.getTime();

            TimeZoneRule toRule = tzt.getTo();
            int ruleIdx = 1;
            for (; ruleIdx < all.length; ruleIdx++) {
                if (all[ruleIdx].equals(toRule)) {
                    break;
                }
            }
            if (ruleIdx >= all.length) {
                throw new IllegalStateException("The rule was not found");
            }
            if (isProcessed.get(ruleIdx)) {
                continue;
            }
            if (toRule instanceof TimeArrayTimeZoneRule) {
                TimeArrayTimeZoneRule tar = (TimeArrayTimeZoneRule) toRule;

                // Get the previous raw offset and DST savings before the very first start time
                long t = start;
                while (true) {
                    tzt = getNextTransition(t, false);
                    if (tzt == null) {
                        break;
                    }
                    if (tzt.getTo().equals(tar)) {
                        break;
                    }
                    t = tzt.getTime();
                }
                if (tzt != null) {
                    // Check if the entire start times to be added
                    Date firstStart =
                            tar.getFirstStart(
                                    tzt.getFrom().getRawOffset(), tzt.getFrom().getDSTSavings());
                    if (firstStart.getTime() > start) {
                        // Just add the rule as is
                        filteredRules.add(tar);
                    } else {
                        // Collect transitions after the start time
                        long[] times = tar.getStartTimes();
                        int timeType = tar.getTimeType();
                        int idx;
                        for (idx = 0; idx < times.length; idx++) {
                            t = times[idx];
                            if (timeType == DateTimeRule.STANDARD_TIME) {
                                t -= tzt.getFrom().getRawOffset();
                            }
                            if (timeType == DateTimeRule.WALL_TIME) {
                                t -= tzt.getFrom().getDSTSavings();
                            }
                            if (t > start) {
                                break;
                            }
                        }
                        int asize = times.length - idx;
                        if (asize > 0) {
                            long[] newtimes = new long[asize];
                            System.arraycopy(times, idx, newtimes, 0, asize);
                            TimeArrayTimeZoneRule newtar =
                                    new TimeArrayTimeZoneRule(
                                            tar.getName(),
                                            tar.getRawOffset(),
                                            tar.getDSTSavings(),
                                            newtimes,
                                            tar.getTimeType());
                            filteredRules.add(newtar);
                        }
                    }
                }
            } else if (toRule instanceof AnnualTimeZoneRule) {
                AnnualTimeZoneRule ar = (AnnualTimeZoneRule) toRule;
                Date firstStart =
                        ar.getFirstStart(
                                tzt.getFrom().getRawOffset(), tzt.getFrom().getDSTSavings());
                if (firstStart.getTime() == tzt.getTime()) {
                    // Just add the rule as is
                    filteredRules.add(ar);
                } else {
                    // Calculate the transition year
                    // Recreate the rule
                    AnnualTimeZoneRule newar =
                            new AnnualTimeZoneRule(
                                    ar.getName(),
                                    ar.getRawOffset(),
                                    ar.getDSTSavings(),
                                    ar.getRule(),
                                    Grego.timeToYear(tzt.getTime()),
                                    ar.getEndYear());
                    filteredRules.add(newar);
                }
                // Check if this is a final rule
                if (ar.getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
                    // After both final standard and dst rule are processed,
                    // exit this while loop.
                    if (ar.getDSTSavings() == 0) {
                        bFinalStd = true;
                    } else {
                        bFinalDst = true;
                    }
                }
            }
            isProcessed.set(ruleIdx);
        }
        TimeZoneRule[] rules = filteredRules.toArray(new TimeZoneRule[filteredRules.size()]);
        return rules;
    }

    /**
     * {@icu} Returns the array of <code>TimeZoneRule</code> which represents the rule of this time
     * zone object near the specified date. Some applications are not capable to handle historic
     * time zone rule changes. Also some applications can only handle certain type of rule
     * definitions. This method returns either a single <code>InitialTimeZoneRule</code> if this
     * time zone does not have any daylight saving time within 1 year from the specified time, or a
     * pair of <code>AnnualTimeZoneRule</code> whose rule type is <code>DateTimeRule.DOW</code> for
     * date and <code>DateTimeRule.WALL_TIME</code> for time with a single <code>InitialTimeZoneRule
     * </code> representing the initial time, when this time zone observes daylight saving time near
     * the specified date. Thus, the result may be only valid for dates around the specified date.
     *
     * @param date The date to be used for <code>TimeZoneRule</code> extraction.
     * @return The array of <code>TimeZoneRule</code>, either a single <code>InitialTimeZoneRule
     *     </code> object, or a pair of <code>AnnualTimeZoneRule</code> with a single <code>
     *     InitialTimeZoneRule</code>. The first element in the array is always a <code>
     *     InitialTimeZoneRule</code>.
     * @stable ICU 3.8
     */
    public TimeZoneRule[] getSimpleTimeZoneRulesNear(long date) {
        AnnualTimeZoneRule[] annualRules = null;
        TimeZoneRule initialRule = null;
        // Get the next transition
        TimeZoneTransition tr = getNextTransition(date, false);
        if (tr != null) {
            String initialName = tr.getFrom().getName();
            int initialRaw = tr.getFrom().getRawOffset();
            int initialDst = tr.getFrom().getDSTSavings();

            // Check if the next transition is either DST->STD or STD->DST and
            // within roughly 1 year from the specified date
            long nextTransitionTime = tr.getTime();
            if (((tr.getFrom().getDSTSavings() == 0 && tr.getTo().getDSTSavings() != 0)
                            || (tr.getFrom().getDSTSavings() != 0
                                    && tr.getTo().getDSTSavings() == 0))
                    && date + MILLIS_PER_YEAR > nextTransitionTime) {
                annualRules = new AnnualTimeZoneRule[2];
                // Get local wall time for the transition time
                int dtfields[] =
                        Grego.timeToFields(
                                nextTransitionTime
                                        + tr.getFrom().getRawOffset()
                                        + tr.getFrom().getDSTSavings(),
                                null);
                int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]);
                // Create DOW rule
                DateTimeRule dtr =
                        new DateTimeRule(
                                dtfields[1],
                                weekInMonth,
                                dtfields[3],
                                dtfields[5],
                                DateTimeRule.WALL_TIME);

                AnnualTimeZoneRule secondRule = null;

                // Note:  SimpleTimeZone does not support raw offset change.
                // So we always use raw offset of the given time for the rule,
                // even raw offset is changed.  This will result that the result
                // zone to return wrong offset after the transition.
                // When we encounter such case, we do not inspect next next
                // transition for another rule.
                annualRules[0] =
                        new AnnualTimeZoneRule(
                                tr.getTo().getName(),
                                initialRaw,
                                tr.getTo().getDSTSavings(),
                                dtr,
                                dtfields[0],
                                AnnualTimeZoneRule.MAX_YEAR);

                if (tr.getTo().getRawOffset() == initialRaw) {

                    // Get the next next transition
                    tr = getNextTransition(nextTransitionTime, false);
                    if (tr != null) {
                        // Check if the next next transition is either DST->STD or STD->DST
                        // and within roughly 1 year from the next transition
                        if (((tr.getFrom().getDSTSavings() == 0 && tr.getTo().getDSTSavings() != 0)
                                        || (tr.getFrom().getDSTSavings() != 0
                                                && tr.getTo().getDSTSavings() == 0))
                                && nextTransitionTime + MILLIS_PER_YEAR > tr.getTime()) {
                            // Generate another DOW rule
                            dtfields =
                                    Grego.timeToFields(
                                            tr.getTime()
                                                    + tr.getFrom().getRawOffset()
                                                    + tr.getFrom().getDSTSavings(),
                                            dtfields);
                            weekInMonth =
                                    Grego.getDayOfWeekInMonth(
                                            dtfields[0], dtfields[1], dtfields[2]);
                            dtr =
                                    new DateTimeRule(
                                            dtfields[1],
                                            weekInMonth,
                                            dtfields[3],
                                            dtfields[5],
                                            DateTimeRule.WALL_TIME);
                            secondRule =
                                    new AnnualTimeZoneRule(
                                            tr.getTo().getName(),
                                            tr.getTo().getRawOffset(),
                                            tr.getTo().getDSTSavings(),
                                            dtr,
                                            dtfields[0] - 1,
                                            AnnualTimeZoneRule.MAX_YEAR);
                            // Make sure this rule can be applied to the specified date
                            Date d =
                                    secondRule.getPreviousStart(
                                            date,
                                            tr.getFrom().getRawOffset(),
                                            tr.getFrom().getDSTSavings(),
                                            true);
                            if (d != null
                                    && d.getTime() <= date
                                    && initialRaw == tr.getTo().getRawOffset()
                                    && initialDst == tr.getTo().getDSTSavings()) {
                                // We can use this rule as the second transition rule
                                annualRules[1] = secondRule;
                            }
                        }
                    }
                }

                if (annualRules[1] == null) {
                    // Try previous transition
                    tr = getPreviousTransition(date, true);
                    if (tr != null) {
                        // Check if the previous transition is either DST->STD or STD->DST.
                        // The actual transition time does not matter here.
                        if ((tr.getFrom().getDSTSavings() == 0 && tr.getTo().getDSTSavings() != 0)
                                || (tr.getFrom().getDSTSavings() != 0
                                        && tr.getTo().getDSTSavings() == 0)) {
                            // Generate another DOW rule
                            dtfields =
                                    Grego.timeToFields(
                                            tr.getTime()
                                                    + tr.getFrom().getRawOffset()
                                                    + tr.getFrom().getDSTSavings(),
                                            dtfields);
                            weekInMonth =
                                    Grego.getDayOfWeekInMonth(
                                            dtfields[0], dtfields[1], dtfields[2]);
                            dtr =
                                    new DateTimeRule(
                                            dtfields[1],
                                            weekInMonth,
                                            dtfields[3],
                                            dtfields[5],
                                            DateTimeRule.WALL_TIME);

                            // second rule raw/dst offsets should match raw/dst offsets
                            // at the given time
                            secondRule =
                                    new AnnualTimeZoneRule(
                                            tr.getTo().getName(),
                                            initialRaw,
                                            initialDst,
                                            dtr,
                                            annualRules[0].getStartYear() - 1,
                                            AnnualTimeZoneRule.MAX_YEAR);

                            // Check if this rule start after the first rule after the
                            // specified date
                            Date d =
                                    secondRule.getNextStart(
                                            date,
                                            tr.getFrom().getRawOffset(),
                                            tr.getFrom().getDSTSavings(),
                                            false);
                            if (d.getTime() > nextTransitionTime) {
                                // We can use this rule as the second transition rule
                                annualRules[1] = secondRule;
                            }
                        }
                    }
                }
                if (annualRules[1] == null) {
                    // Cannot generate a good pair of AnnualTimeZoneRule
                    annualRules = null;
                } else {
                    // The initial rule should represent the rule before the previous transition
                    initialName = annualRules[0].getName();
                    initialRaw = annualRules[0].getRawOffset();
                    initialDst = annualRules[0].getDSTSavings();
                }
            }
            initialRule = new InitialTimeZoneRule(initialName, initialRaw, initialDst);
        } else {
            // Try the previous one
            tr = getPreviousTransition(date, true);
            if (tr != null) {
                initialRule =
                        new InitialTimeZoneRule(
                                tr.getTo().getName(),
                                tr.getTo().getRawOffset(),
                                tr.getTo().getDSTSavings());
            } else {
                // No transitions in the past.  Just use the current offsets
                int[] offsets = new int[2];
                getOffset(date, false, offsets);
                initialRule = new InitialTimeZoneRule(getID(), offsets[0], offsets[1]);
            }
        }

        TimeZoneRule[] result = null;
        if (annualRules == null) {
            result = new TimeZoneRule[1];
            result[0] = initialRule;
        } else {
            result = new TimeZoneRule[3];
            result[0] = initialRule;
            result[1] = annualRules[0];
            result[2] = annualRules[1];
        }

        return result;
    }

    /**
     * {@icu} Options used by {@link #getOffsetFromLocal(long, LocalOption, LocalOption, int[])} to
     * specify how to interpret an input time when it does not exist, or when it is ambiguous,
     * around a time zone transition.
     *
     * @stable ICU 69
     */
    public static enum LocalOption {
        /**
         * An input time is always interpreted as local time before a time zone transition.
         *
         * @stable ICU 69
         */
        FORMER(0x04),
        /**
         * An input time is always interpreted as local time after a time zone transition.
         *
         * @stable ICU 69
         */
        LATTER(0x0C),
        /**
         * An input time is interpreted as standard time when local time is switched to/from
         * daylight saving time. When both sides of a time zone transition are standard time, or
         * daylight saving time, the local time before the transition is used.
         *
         * @stable ICU 69
         */
        STANDARD_FORMER(0x05),
        /**
         * An input time is interpreted as standard time when local time is switched to/from
         * daylight saving time. When both sides of a time zone transition are standard time, or
         * daylight saving time, the local time after the transition is used.
         *
         * @stable ICU 69
         */
        STANDARD_LATTER(0x0D),
        /**
         * An input time is interpreted as daylight saving time when local time is switched to/from
         * standard time. When both sides of a time zone transition are standard time, or daylight
         * saving time, the local time before the transition is used.
         *
         * @stable ICU 69
         */
        DAYLIGHT_FORMER(0x07),
        /**
         * An input time is interpreted as daylight saving time when local time is switched to/from
         * standard time. When both sides of a time zone transition are standard time, or daylight
         * saving time, the local time after the transition is used.
         *
         * @stable ICU 69
         */
        DAYLIGHT_LATTER(0x0F);

        private int flagVal;

        LocalOption(int flagVal) {
            this.flagVal = flagVal;
        }
    }

    /**
     * Get {@link LocalOption}'s internal flag value. This is used by ICU internal implementation
     * only.
     *
     * @param locOpt A LocalOption
     * @return LocalOption's internal flag value.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    protected static int getLocalOptionValue(LocalOption locOpt) {
        return locOpt.flagVal;
    }

    /**
     * The time type option for standard time used by internal implementation.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final int LOCAL_STD = 0x01;

    /**
     * The time type option for daylight saving time used internally.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final int LOCAL_DST = 0x03;

    /**
     * The option designate former time used by internal implementation.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final int LOCAL_FORMER = 0x04;

    /**
     * The option designate latter time used by internal implementation.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final int LOCAL_LATTER = 0x0C;

    /**
     * The bit mask for the time type option used by internal implementation.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final int STD_DST_MASK = 0x03;

    /**
     * The bit mask for the former/latter option used by internal implementation.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final int FORMER_LATTER_MASK = 0x0C;

    /**
     * {@icu} Returns time zone offsets from local wall time.
     *
     * @stable ICU 69
     */
    public void getOffsetFromLocal(
            long date,
            LocalOption nonExistingTimeOpt,
            LocalOption duplicatedTimeOpt,
            int[] offsets) {
        throw new IllegalStateException("Not implemented");
    }

    /**
     * Protected no arg constructor.
     *
     * @stable ICU 3.8
     */
    protected BasicTimeZone() {}

    /**
     * Constructing a BasicTimeZone with the given time zone ID.
     *
     * @param ID the time zone ID.
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    protected BasicTimeZone(String ID) {
        super(ID);
    }
}