alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Java example source code file (DateTimeZoneBuilder.java)

This example Java source code file (DateTimeZoneBuilder.java) is included in the alvinalexander.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Learn more about this Java project at its project page.

Java - Java tags/keywords

arraylist, chronology, datetimezone, datetimezonebuilder, dstzone, illegalargumentexception, ioexception, ofyear, precalculatedzone, recurrence, rule, ruleset, string, text, transition, util

The DateTimeZoneBuilder.java Java example source code

/*
 *  Copyright 2001-2013 Stephen Colebourne
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.joda.time.tz;

import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DateFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Set;

import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.Period;
import org.joda.time.PeriodType;
import org.joda.time.chrono.ISOChronology;

/**
 * DateTimeZoneBuilder allows complex DateTimeZones to be constructed. Since
 * creating a new DateTimeZone this way is a relatively expensive operation,
 * built zones can be written to a file. Reading back the encoded data is a
 * quick operation.
 * <p>
 * DateTimeZoneBuilder itself is mutable and not thread-safe, but the
 * DateTimeZone objects that it builds are thread-safe and immutable.
 * <p>
 * It is intended that {@link ZoneInfoCompiler} be used to read time zone data
 * files, indirectly calling DateTimeZoneBuilder. The following complex
 * example defines the America/Los_Angeles time zone, with all historical
 * transitions:
 * 
 * <pre>
 * DateTimeZone America_Los_Angeles = new DateTimeZoneBuilder()
 *     .addCutover(-2147483648, 'w', 1, 1, 0, false, 0)
 *     .setStandardOffset(-28378000)
 *     .setFixedSavings("LMT", 0)
 *     .addCutover(1883, 'w', 11, 18, 0, false, 43200000)
 *     .setStandardOffset(-28800000)
 *     .addRecurringSavings("PDT", 3600000, 1918, 1919, 'w',  3, -1, 7, false, 7200000)
 *     .addRecurringSavings("PST",       0, 1918, 1919, 'w', 10, -1, 7, false, 7200000)
 *     .addRecurringSavings("PWT", 3600000, 1942, 1942, 'w',  2,  9, 0, false, 7200000)
 *     .addRecurringSavings("PPT", 3600000, 1945, 1945, 'u',  8, 14, 0, false, 82800000)
 *     .addRecurringSavings("PST",       0, 1945, 1945, 'w',  9, 30, 0, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1948, 1948, 'w',  3, 14, 0, false, 7200000)
 *     .addRecurringSavings("PST",       0, 1949, 1949, 'w',  1,  1, 0, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1950, 1966, 'w',  4, -1, 7, false, 7200000)
 *     .addRecurringSavings("PST",       0, 1950, 1961, 'w',  9, -1, 7, false, 7200000)
 *     .addRecurringSavings("PST",       0, 1962, 1966, 'w', 10, -1, 7, false, 7200000)
 *     .addRecurringSavings("PST",       0, 1967, 2147483647, 'w', 10, -1, 7, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1967, 1973, 'w', 4, -1,  7, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1974, 1974, 'w', 1,  6,  0, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1975, 1975, 'w', 2, 23,  0, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1976, 1986, 'w', 4, -1,  7, false, 7200000)
 *     .addRecurringSavings("PDT", 3600000, 1987, 2147483647, 'w', 4, 1, 7, true, 7200000)
 *     .toDateTimeZone("America/Los_Angeles", true);
 * </pre>
 *
 * @author Brian S O'Neill
 * @see ZoneInfoCompiler
 * @see ZoneInfoProvider
 * @since 1.0
 */
public class DateTimeZoneBuilder {
    /**
     * Decodes a built DateTimeZone from the given stream, as encoded by
     * writeTo.
     *
     * @param in input stream to read encoded DateTimeZone from.
     * @param id time zone id to assign
     */
    public static DateTimeZone readFrom(InputStream in, String id) throws IOException {
        if (in instanceof DataInput) {
            return readFrom((DataInput)in, id);
        } else {
            return readFrom((DataInput)new DataInputStream(in), id);
        }
    }

    /**
     * Decodes a built DateTimeZone from the given stream, as encoded by
     * writeTo.
     *
     * @param in input stream to read encoded DateTimeZone from.
     * @param id time zone id to assign
     */
    public static DateTimeZone readFrom(DataInput in, String id) throws IOException {
        switch (in.readUnsignedByte()) {
        case 'F':
            DateTimeZone fixed = new FixedDateTimeZone
                (id, in.readUTF(), (int)readMillis(in), (int)readMillis(in));
            if (fixed.equals(DateTimeZone.UTC)) {
                fixed = DateTimeZone.UTC;
            }
            return fixed;
        case 'C':
            return CachedDateTimeZone.forZone(PrecalculatedZone.readFrom(in, id));
        case 'P':
            return PrecalculatedZone.readFrom(in, id);
        default:
            throw new IOException("Invalid encoding");
        }
    }

    /**
     * Millisecond encoding formats:
     *
     * upper two bits  units       field length  approximate range
     * ---------------------------------------------------------------
     * 00              30 minutes  1 byte        +/- 16 hours
     * 01              minutes     4 bytes       +/- 1020 years
     * 10              seconds     5 bytes       +/- 4355 years
     * 11              millis      9 bytes       +/- 292,000,000 years
     *
     * Remaining bits in field form signed offset from 1970-01-01T00:00:00Z.
     */
    static void writeMillis(DataOutput out, long millis) throws IOException {
        if (millis % (30 * 60000L) == 0) {
            // Try to write in 30 minute units.
            long units = millis / (30 * 60000L);
            if (((units << (64 - 6)) >> (64 - 6)) == units) {
                // Form 00 (6 bits effective precision)
                out.writeByte((int)(units & 0x3f));
                return;
            }
        }

        if (millis % 60000L == 0) {
            // Try to write minutes.
            long minutes = millis / 60000L;
            if (((minutes << (64 - 30)) >> (64 - 30)) == minutes) {
                // Form 01 (30 bits effective precision)
                out.writeInt(0x40000000 | (int)(minutes & 0x3fffffff));
                return;
            }
        }
        
        if (millis % 1000L == 0) {
            // Try to write seconds.
            long seconds = millis / 1000L;
            if (((seconds << (64 - 38)) >> (64 - 38)) == seconds) {
                // Form 10 (38 bits effective precision)
                out.writeByte(0x80 | (int)((seconds >> 32) & 0x3f));
                out.writeInt((int)(seconds & 0xffffffff));
                return;
            }
        }

        // Write milliseconds either because the additional precision is
        // required or the minutes didn't fit in the field.
        
        // Form 11 (64-bits effective precision, but write as if 70 bits)
        out.writeByte(millis < 0 ? 0xff : 0xc0);
        out.writeLong(millis);
    }

    /**
     * Reads encoding generated by writeMillis.
     */
    static long readMillis(DataInput in) throws IOException {
        int v = in.readUnsignedByte();
        switch (v >> 6) {
        case 0: default:
            // Form 00 (6 bits effective precision)
            v = (v << (32 - 6)) >> (32 - 6);
            return v * (30 * 60000L);

        case 1:
            // Form 01 (30 bits effective precision)
            v = (v << (32 - 6)) >> (32 - 30);
            v |= (in.readUnsignedByte()) << 16;
            v |= (in.readUnsignedByte()) << 8;
            v |= (in.readUnsignedByte());
            return v * 60000L;

        case 2:
            // Form 10 (38 bits effective precision)
            long w = (((long)v) << (64 - 6)) >> (64 - 38);
            w |= (in.readUnsignedByte()) << 24;
            w |= (in.readUnsignedByte()) << 16;
            w |= (in.readUnsignedByte()) << 8;
            w |= (in.readUnsignedByte());
            return w * 1000L;

        case 3:
            // Form 11 (64-bits effective precision)
            return in.readLong();
        }
    }

    private static DateTimeZone buildFixedZone(String id, String nameKey,
                                               int wallOffset, int standardOffset) {
        if ("UTC".equals(id) && id.equals(nameKey) &&
            wallOffset == 0 && standardOffset == 0) {
            return DateTimeZone.UTC;
        }
        return new FixedDateTimeZone(id, nameKey, wallOffset, standardOffset);
    }

    // List of RuleSets.
    private final ArrayList<RuleSet> iRuleSets;

    public DateTimeZoneBuilder() {
        iRuleSets = new ArrayList<RuleSet>(10);
    }

    /**
     * Adds a cutover for added rules. The standard offset at the cutover
     * defaults to 0. Call setStandardOffset afterwards to change it.
     *
     * @param year  the year of cutover
     * @param mode 'u' - cutover is measured against UTC, 'w' - against wall
     *  offset, 's' - against standard offset
     * @param monthOfYear  the month from 1 (January) to 12 (December)
     * @param dayOfMonth  if negative, set to ((last day of month) - ~dayOfMonth).
     *  For example, if -1, set to last day of month
     * @param dayOfWeek  from 1 (Monday) to 7 (Sunday), if 0 then ignore
     * @param advanceDayOfWeek  if dayOfMonth does not fall on dayOfWeek, advance to
     *  dayOfWeek when true, retreat when false.
     * @param millisOfDay  additional precision for specifying time of day of cutover
     */
    public DateTimeZoneBuilder addCutover(int year,
                                          char mode,
                                          int monthOfYear,
                                          int dayOfMonth,
                                          int dayOfWeek,
                                          boolean advanceDayOfWeek,
                                          int millisOfDay)
    {
        if (iRuleSets.size() > 0) {
            OfYear ofYear = new OfYear
                (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay);
            RuleSet lastRuleSet = iRuleSets.get(iRuleSets.size() - 1);
            lastRuleSet.setUpperLimit(year, ofYear);
        }
        iRuleSets.add(new RuleSet());
        return this;
    }

    /**
     * Sets the standard offset to use for newly added rules until the next
     * cutover is added.
     * @param standardOffset  the standard offset in millis
     */
    public DateTimeZoneBuilder setStandardOffset(int standardOffset) {
        getLastRuleSet().setStandardOffset(standardOffset);
        return this;
    }

    /**
     * Set a fixed savings rule at the cutover.
     */
    public DateTimeZoneBuilder setFixedSavings(String nameKey, int saveMillis) {
        getLastRuleSet().setFixedSavings(nameKey, saveMillis);
        return this;
    }

    /**
     * Add a recurring daylight saving time rule.
     *
     * @param nameKey  the name key of new rule
     * @param saveMillis  the milliseconds to add to standard offset
     * @param fromYear  the first year that rule is in effect, MIN_VALUE indicates
     * beginning of time
     * @param toYear  the last year (inclusive) that rule is in effect, MAX_VALUE
     *  indicates end of time
     * @param mode  'u' - transitions are calculated against UTC, 'w' -
     *  transitions are calculated against wall offset, 's' - transitions are
     *  calculated against standard offset
     * @param monthOfYear  the month from 1 (January) to 12 (December)
     * @param dayOfMonth  if negative, set to ((last day of month) - ~dayOfMonth).
     *  For example, if -1, set to last day of month
     * @param dayOfWeek  from 1 (Monday) to 7 (Sunday), if 0 then ignore
     * @param advanceDayOfWeek  if dayOfMonth does not fall on dayOfWeek, advance to
     *  dayOfWeek when true, retreat when false.
     * @param millisOfDay  additional precision for specifying time of day of transitions
     */
    public DateTimeZoneBuilder addRecurringSavings(String nameKey, int saveMillis,
                                                   int fromYear, int toYear,
                                                   char mode,
                                                   int monthOfYear,
                                                   int dayOfMonth,
                                                   int dayOfWeek,
                                                   boolean advanceDayOfWeek,
                                                   int millisOfDay)
    {
        if (fromYear <= toYear) {
            OfYear ofYear = new OfYear
                (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay);
            Recurrence recurrence = new Recurrence(ofYear, nameKey, saveMillis);
            Rule rule = new Rule(recurrence, fromYear, toYear);
            getLastRuleSet().addRule(rule);
        }
        return this;
    }

    private RuleSet getLastRuleSet() {
        if (iRuleSets.size() == 0) {
            addCutover(Integer.MIN_VALUE, 'w', 1, 1, 0, false, 0);
        }
        return iRuleSets.get(iRuleSets.size() - 1);
    }
    
    /**
     * Processes all the rules and builds a DateTimeZone.
     *
     * @param id  time zone id to assign
     * @param outputID  true if the zone id should be output
     */
    public DateTimeZone toDateTimeZone(String id, boolean outputID) {
        if (id == null) {
            throw new IllegalArgumentException();
        }

        // Discover where all the transitions occur and store the results in
        // these lists.
        ArrayList<Transition> transitions = new ArrayList();

        // Tail zone picks up remaining transitions in the form of an endless
        // DST cycle.
        DSTZone tailZone = null;

        long millis = Long.MIN_VALUE;
        int saveMillis = 0;
            
        int ruleSetCount = iRuleSets.size();
        for (int i=0; i<ruleSetCount; i++) {
            RuleSet rs = iRuleSets.get(i);
            Transition next = rs.firstTransition(millis);
            if (next == null) {
                continue;
            }
            addTransition(transitions, next);
            millis = next.getMillis();
            saveMillis = next.getSaveMillis();

            // Copy it since we're going to destroy it.
            rs = new RuleSet(rs);

            while ((next = rs.nextTransition(millis, saveMillis)) != null) {
                if (addTransition(transitions, next) && tailZone != null) {
                    // Got the extra transition before DSTZone.
                    break;
                }
                millis = next.getMillis();
                saveMillis = next.getSaveMillis();
                if (tailZone == null && i == ruleSetCount - 1) {
                    tailZone = rs.buildTailZone(id);
                    // If tailZone is not null, don't break out of main loop until
                    // at least one more transition is calculated. This ensures a
                    // correct 'seam' to the DSTZone.
                }
            }

            millis = rs.getUpperLimit(saveMillis);
        }

        // Check if a simpler zone implementation can be returned.
        if (transitions.size() == 0) {
            if (tailZone != null) {
                // This shouldn't happen, but handle just in case.
                return tailZone;
            }
            return buildFixedZone(id, "UTC", 0, 0);
        }
        if (transitions.size() == 1 && tailZone == null) {
            Transition tr = transitions.get(0);
            return buildFixedZone(id, tr.getNameKey(),
                                  tr.getWallOffset(), tr.getStandardOffset());
        }

        PrecalculatedZone zone = PrecalculatedZone.create(id, outputID, transitions, tailZone);
        if (zone.isCachable()) {
            return CachedDateTimeZone.forZone(zone);
        }
        return zone;
    }

    private boolean addTransition(ArrayList<Transition> transitions, Transition tr) {
        int size = transitions.size();
        if (size == 0) {
            transitions.add(tr);
            return true;
        }

        Transition last = transitions.get(size - 1);
        if (!tr.isTransitionFrom(last)) {
            return false;
        }

        // If local time of new transition is same as last local time, just
        // replace last transition with new one.
        int offsetForLast = 0;
        if (size >= 2) {
            offsetForLast = transitions.get(size - 2).getWallOffset();
        }
        int offsetForNew = last.getWallOffset();

        long lastLocal = last.getMillis() + offsetForLast;
        long newLocal = tr.getMillis() + offsetForNew;

        if (newLocal != lastLocal) {
            transitions.add(tr);
            return true;
        }

        transitions.remove(size - 1);
        return addTransition(transitions, tr);
    }

    /**
     * Encodes a built DateTimeZone to the given stream. Call readFrom to
     * decode the data into a DateTimeZone object.
     *
     * @param out  the output stream to receive the encoded DateTimeZone
     * @since 1.5 (parameter added)
     */
    public void writeTo(String zoneID, OutputStream out) throws IOException {
        if (out instanceof DataOutput) {
            writeTo(zoneID, (DataOutput)out);
        } else {
            DataOutputStream dout = new DataOutputStream(out);
            writeTo(zoneID, (DataOutput)dout);
            dout.flush();
        }
    }

    /**
     * Encodes a built DateTimeZone to the given stream. Call readFrom to
     * decode the data into a DateTimeZone object.
     *
     * @param out  the output stream to receive the encoded DateTimeZone
     * @since 1.5 (parameter added)
     */
    public void writeTo(String zoneID, DataOutput out) throws IOException {
        // pass false so zone id is not written out
        DateTimeZone zone = toDateTimeZone(zoneID, false);

        if (zone instanceof FixedDateTimeZone) {
            out.writeByte('F'); // 'F' for fixed
            out.writeUTF(zone.getNameKey(0));
            writeMillis(out, zone.getOffset(0));
            writeMillis(out, zone.getStandardOffset(0));
        } else {
            if (zone instanceof CachedDateTimeZone) {
                out.writeByte('C'); // 'C' for cached, precalculated
                zone = ((CachedDateTimeZone)zone).getUncachedZone();
            } else {
                out.writeByte('P'); // 'P' for precalculated, uncached
            }
            ((PrecalculatedZone)zone).writeTo(out);
        }
    }

    /**
     * Supports setting fields of year and moving between transitions.
     */
    private static final class OfYear {
        static OfYear readFrom(DataInput in) throws IOException {
            return new OfYear((char)in.readUnsignedByte(),
                              (int)in.readUnsignedByte(),
                              (int)in.readByte(),
                              (int)in.readUnsignedByte(),
                              in.readBoolean(),
                              (int)readMillis(in));
        }

        // Is 'u', 'w', or 's'.
        final char iMode;

        final int iMonthOfYear;
        final int iDayOfMonth;
        final int iDayOfWeek;
        final boolean iAdvance;
        final int iMillisOfDay;

        OfYear(char mode,
               int monthOfYear,
               int dayOfMonth,
               int dayOfWeek, boolean advanceDayOfWeek,
               int millisOfDay)
        {
            if (mode != 'u' && mode != 'w' && mode != 's') {
                throw new IllegalArgumentException("Unknown mode: " + mode);
            }

            iMode = mode;
            iMonthOfYear = monthOfYear;
            iDayOfMonth = dayOfMonth;
            iDayOfWeek = dayOfWeek;
            iAdvance = advanceDayOfWeek;
            iMillisOfDay = millisOfDay;
        }

        /**
         * @param standardOffset standard offset just before instant
         */
        public long setInstant(int year, int standardOffset, int saveMillis) {
            int offset;
            if (iMode == 'w') {
                offset = standardOffset + saveMillis;
            } else if (iMode == 's') {
                offset = standardOffset;
            } else {
                offset = 0;
            }

            Chronology chrono = ISOChronology.getInstanceUTC();
            long millis = chrono.year().set(0, year);
            millis = chrono.monthOfYear().set(millis, iMonthOfYear);
            millis = chrono.millisOfDay().set(millis, iMillisOfDay);
            millis = setDayOfMonth(chrono, millis);

            if (iDayOfWeek != 0) {
                millis = setDayOfWeek(chrono, millis);
            }

            // Convert from local time to UTC.
            return millis - offset;
        }

        /**
         * @param standardOffset standard offset just before next recurrence
         */
        public long next(long instant, int standardOffset, int saveMillis) {
            int offset;
            if (iMode == 'w') {
                offset = standardOffset + saveMillis;
            } else if (iMode == 's') {
                offset = standardOffset;
            } else {
                offset = 0;
            }

            // Convert from UTC to local time.
            instant += offset;

            Chronology chrono = ISOChronology.getInstanceUTC();
            long next = chrono.monthOfYear().set(instant, iMonthOfYear);
            // Be lenient with millisOfDay.
            next = chrono.millisOfDay().set(next, 0);
            next = chrono.millisOfDay().add(next, iMillisOfDay);
            next = setDayOfMonthNext(chrono, next);

            if (iDayOfWeek == 0) {
                if (next <= instant) {
                    next = chrono.year().add(next, 1);
                    next = setDayOfMonthNext(chrono, next);
                }
            } else {
                next = setDayOfWeek(chrono, next);
                if (next <= instant) {
                    next = chrono.year().add(next, 1);
                    next = chrono.monthOfYear().set(next, iMonthOfYear);
                    next = setDayOfMonthNext(chrono, next);
                    next = setDayOfWeek(chrono, next);
                }
            }

            // Convert from local time to UTC.
            return next - offset;
        }

        /**
         * @param standardOffset standard offset just before previous recurrence
         */
        public long previous(long instant, int standardOffset, int saveMillis) {
            int offset;
            if (iMode == 'w') {
                offset = standardOffset + saveMillis;
            } else if (iMode == 's') {
                offset = standardOffset;
            } else {
                offset = 0;
            }

            // Convert from UTC to local time.
            instant += offset;

            Chronology chrono = ISOChronology.getInstanceUTC();
            long prev = chrono.monthOfYear().set(instant, iMonthOfYear);
            // Be lenient with millisOfDay.
            prev = chrono.millisOfDay().set(prev, 0);
            prev = chrono.millisOfDay().add(prev, iMillisOfDay);
            prev = setDayOfMonthPrevious(chrono, prev);

            if (iDayOfWeek == 0) {
                if (prev >= instant) {
                    prev = chrono.year().add(prev, -1);
                    prev = setDayOfMonthPrevious(chrono, prev);
                }
            } else {
                prev = setDayOfWeek(chrono, prev);
                if (prev >= instant) {
                    prev = chrono.year().add(prev, -1);
                    prev = chrono.monthOfYear().set(prev, iMonthOfYear);
                    prev = setDayOfMonthPrevious(chrono, prev);
                    prev = setDayOfWeek(chrono, prev);
                }
            }

            // Convert from local time to UTC.
            return prev - offset;
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof OfYear) {
                OfYear other = (OfYear)obj;
                return
                    iMode == other.iMode &&
                    iMonthOfYear == other.iMonthOfYear &&
                    iDayOfMonth == other.iDayOfMonth &&
                    iDayOfWeek == other.iDayOfWeek &&
                    iAdvance == other.iAdvance &&
                    iMillisOfDay == other.iMillisOfDay;
            }
            return false;
        }

        /*
        public String toString() {
            return
                "[OfYear]\n" + 
                "Mode: " + iMode + '\n' +
                "MonthOfYear: " + iMonthOfYear + '\n' +
                "DayOfMonth: " + iDayOfMonth + '\n' +
                "DayOfWeek: " + iDayOfWeek + '\n' +
                "AdvanceDayOfWeek: " + iAdvance + '\n' +
                "MillisOfDay: " + iMillisOfDay + '\n';
        }
        */

        public void writeTo(DataOutput out) throws IOException {
            out.writeByte(iMode);
            out.writeByte(iMonthOfYear);
            out.writeByte(iDayOfMonth);
            out.writeByte(iDayOfWeek);
            out.writeBoolean(iAdvance);
            writeMillis(out, iMillisOfDay);
        }

        /**
         * If month-day is 02-29 and year isn't leap, advances to next leap year.
         */
        private long setDayOfMonthNext(Chronology chrono, long next) {
            try {
                next = setDayOfMonth(chrono, next);
            } catch (IllegalArgumentException e) {
                if (iMonthOfYear == 2 && iDayOfMonth == 29) {
                    while (chrono.year().isLeap(next) == false) {
                        next = chrono.year().add(next, 1);
                    }
                    next = setDayOfMonth(chrono, next);
                } else {
                    throw e;
                }
            }
            return next;
        }

        /**
         * If month-day is 02-29 and year isn't leap, retreats to previous leap year.
         */
        private long setDayOfMonthPrevious(Chronology chrono, long prev) {
            try {
                prev = setDayOfMonth(chrono, prev);
            } catch (IllegalArgumentException e) {
                if (iMonthOfYear == 2 && iDayOfMonth == 29) {
                    while (chrono.year().isLeap(prev) == false) {
                        prev = chrono.year().add(prev, -1);
                    }
                    prev = setDayOfMonth(chrono, prev);
                } else {
                    throw e;
                }
            }
            return prev;
        }

        private long setDayOfMonth(Chronology chrono, long instant) {
            if (iDayOfMonth >= 0) {
                instant = chrono.dayOfMonth().set(instant, iDayOfMonth);
            } else {
                instant = chrono.dayOfMonth().set(instant, 1);
                instant = chrono.monthOfYear().add(instant, 1);
                instant = chrono.dayOfMonth().add(instant, iDayOfMonth);
            }
            return instant;
        }

        private long setDayOfWeek(Chronology chrono, long instant) {
            int dayOfWeek = chrono.dayOfWeek().get(instant);
            int daysToAdd = iDayOfWeek - dayOfWeek;
            if (daysToAdd != 0) {
                if (iAdvance) {
                    if (daysToAdd < 0) {
                        daysToAdd += 7;
                    }
                } else {
                    if (daysToAdd > 0) {
                        daysToAdd -= 7;
                    }
                }
                instant = chrono.dayOfWeek().add(instant, daysToAdd);
            }
            return instant;
        }
    }

    /**
     * Extends OfYear with a nameKey and savings.
     */
    private static final class Recurrence {
        static Recurrence readFrom(DataInput in) throws IOException {
            return new Recurrence(OfYear.readFrom(in), in.readUTF(), (int)readMillis(in));
        }

        final OfYear iOfYear;
        final String iNameKey;
        final int iSaveMillis;

        Recurrence(OfYear ofYear, String nameKey, int saveMillis) {
            iOfYear = ofYear;
            iNameKey = nameKey;
            iSaveMillis = saveMillis;
        }

        public OfYear getOfYear() {
            return iOfYear;
        }

        /**
         * @param standardOffset standard offset just before next recurrence
         */
        public long next(long instant, int standardOffset, int saveMillis) {
            return iOfYear.next(instant, standardOffset, saveMillis);
        }

        /**
         * @param standardOffset standard offset just before previous recurrence
         */
        public long previous(long instant, int standardOffset, int saveMillis) {
            return iOfYear.previous(instant, standardOffset, saveMillis);
        }

        public String getNameKey() {
            return iNameKey;
        }

        public int getSaveMillis() {
            return iSaveMillis;
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof Recurrence) {
                Recurrence other = (Recurrence)obj;
                return
                    iSaveMillis == other.iSaveMillis &&
                    iNameKey.equals(other.iNameKey) &&
                    iOfYear.equals(other.iOfYear);
            }
            return false;
        }

        public void writeTo(DataOutput out) throws IOException {
            iOfYear.writeTo(out);
            out.writeUTF(iNameKey);
            writeMillis(out, iSaveMillis);
        }

        Recurrence rename(String nameKey) {
            return new Recurrence(iOfYear, nameKey, iSaveMillis);
        }

        Recurrence renameAppend(String appendNameKey) {
            return rename((iNameKey + appendNameKey).intern());
        }
    }

    /**
     * Extends Recurrence with inclusive year limits.
     */
    private static final class Rule {
        final Recurrence iRecurrence;
        final int iFromYear; // inclusive
        final int iToYear;   // inclusive

        Rule(Recurrence recurrence, int fromYear, int toYear) {
            iRecurrence = recurrence;
            iFromYear = fromYear;
            iToYear = toYear;
        }

        @SuppressWarnings("unused")
        public int getFromYear() {
            return iFromYear;
        }

        public int getToYear() {
            return iToYear;
        }

        @SuppressWarnings("unused")
        public OfYear getOfYear() {
            return iRecurrence.getOfYear();
        }

        public String getNameKey() {
            return iRecurrence.getNameKey();
        }

        public int getSaveMillis() {
            return iRecurrence.getSaveMillis();
        }

        public long next(final long instant, int standardOffset, int saveMillis) {
            Chronology chrono = ISOChronology.getInstanceUTC();

            final int wallOffset = standardOffset + saveMillis;
            long testInstant = instant;

            int year;
            if (instant == Long.MIN_VALUE) {
                year = Integer.MIN_VALUE;
            } else {
                year = chrono.year().get(instant + wallOffset);
            }

            if (year < iFromYear) {
                // First advance instant to start of from year.
                testInstant = chrono.year().set(0, iFromYear) - wallOffset;
                // Back off one millisecond to account for next recurrence
                // being exactly at the beginning of the year.
                testInstant -= 1;
            }

            long next = iRecurrence.next(testInstant, standardOffset, saveMillis);

            if (next > instant) {
                year = chrono.year().get(next + wallOffset);
                if (year > iToYear) {
                    // Out of range, return original value.
                    next = instant;
                }
            }

            return next;
        }
    }

    private static final class Transition {
        private final long iMillis;
        private final String iNameKey;
        private final int iWallOffset;
        private final int iStandardOffset;

        Transition(long millis, Transition tr) {
            iMillis = millis;
            iNameKey = tr.iNameKey;
            iWallOffset = tr.iWallOffset;
            iStandardOffset = tr.iStandardOffset;
        }

        Transition(long millis, Rule rule, int standardOffset) {
            iMillis = millis;
            iNameKey = rule.getNameKey();
            iWallOffset = standardOffset + rule.getSaveMillis();
            iStandardOffset = standardOffset;
        }

        Transition(long millis, String nameKey,
                   int wallOffset, int standardOffset) {
            iMillis = millis;
            iNameKey = nameKey;
            iWallOffset = wallOffset;
            iStandardOffset = standardOffset;
        }

        public long getMillis() {
            return iMillis;
        }

        public String getNameKey() {
            return iNameKey;
        }

        public int getWallOffset() {
            return iWallOffset;
        }

        public int getStandardOffset() {
            return iStandardOffset;
        }

        public int getSaveMillis() {
            return iWallOffset - iStandardOffset;
        }

        /**
         * There must be a change in the millis, wall offsets or name keys.
         */
        public boolean isTransitionFrom(Transition other) {
            if (other == null) {
                return true;
            }
            return iMillis > other.iMillis &&
                (iWallOffset != other.iWallOffset ||
                 //iStandardOffset != other.iStandardOffset ||
                 !(iNameKey.equals(other.iNameKey)));
        }
    }

    private static final class RuleSet {
        private static final int YEAR_LIMIT;

        static {
            // Don't pre-calculate more than 100 years into the future. Almost
            // all zones will stop pre-calculating far sooner anyhow. Either a
            // simple DST cycle is detected or the last rule is a fixed
            // offset. If a zone has a fixed offset set more than 100 years
            // into the future, then it won't be observed.
            long now = DateTimeUtils.currentTimeMillis();
            YEAR_LIMIT = ISOChronology.getInstanceUTC().year().get(now) + 100;
        }

        private int iStandardOffset;
        private ArrayList<Rule> iRules;

        // Optional.
        private String iInitialNameKey;
        private int iInitialSaveMillis;

        // Upper limit is exclusive.
        private int iUpperYear;
        private OfYear iUpperOfYear;

        RuleSet() {
            iRules = new ArrayList<Rule>(10);
            iUpperYear = Integer.MAX_VALUE;
        }

        /**
         * Copy constructor.
         */
        RuleSet(RuleSet rs) {
            iStandardOffset = rs.iStandardOffset;
            iRules = new ArrayList<Rule>(rs.iRules);
            iInitialNameKey = rs.iInitialNameKey;
            iInitialSaveMillis = rs.iInitialSaveMillis;
            iUpperYear = rs.iUpperYear;
            iUpperOfYear = rs.iUpperOfYear;
        }

        @SuppressWarnings("unused")
        public int getStandardOffset() {
            return iStandardOffset;
        }

        public void setStandardOffset(int standardOffset) {
            iStandardOffset = standardOffset;
        }

        public void setFixedSavings(String nameKey, int saveMillis) {
            iInitialNameKey = nameKey;
            iInitialSaveMillis = saveMillis;
        }

        public void addRule(Rule rule) {
            if (!iRules.contains(rule)) {
                iRules.add(rule);
            }
        }

        public void setUpperLimit(int year, OfYear ofYear) {
            iUpperYear = year;
            iUpperOfYear = ofYear;
        }

        /**
         * Returns a transition at firstMillis with the first name key and
         * offsets for this rule set. This method may return null.
         *
         * @param firstMillis millis of first transition
         */
        public Transition firstTransition(final long firstMillis) {
            if (iInitialNameKey != null) {
                // Initial zone info explicitly set, so don't search the rules.
                return new Transition(firstMillis, iInitialNameKey,
                                      iStandardOffset + iInitialSaveMillis, iStandardOffset);
            }

            // Make a copy before we destroy the rules.
            ArrayList<Rule> copy = new ArrayList(iRules);

            // Iterate through all the transitions until firstMillis is
            // reached. Use the name key and savings for whatever rule reaches
            // the limit.

            long millis = Long.MIN_VALUE;
            int saveMillis = 0;
            Transition first = null;

            Transition next;
            while ((next = nextTransition(millis, saveMillis)) != null) {
                millis = next.getMillis();

                if (millis == firstMillis) {
                    first = new Transition(firstMillis, next);
                    break;
                }

                if (millis > firstMillis) {
                    if (first == null) {
                        // Find first rule without savings. This way a more
                        // accurate nameKey is found even though no rule
                        // extends to the RuleSet's lower limit.
                        for (Rule rule : copy) {
                            if (rule.getSaveMillis() == 0) {
                                first = new Transition(firstMillis, rule, iStandardOffset);
                                break;
                            }
                        }
                    }
                    if (first == null) {
                        // Found no rule without savings. Create a transition
                        // with no savings anyhow, and use the best available
                        // name key.
                        first = new Transition(firstMillis, next.getNameKey(),
                                               iStandardOffset, iStandardOffset);
                    }
                    break;
                }
                
                // Set first to the best transition found so far, but next
                // iteration may find something closer to lower limit.
                first = new Transition(firstMillis, next);

                saveMillis = next.getSaveMillis();
            }

            iRules = copy;
            return first;
        }

        /**
         * Returns null if RuleSet is exhausted or upper limit reached. Calling
         * this method will throw away rules as they each become
         * exhausted. Copy the RuleSet before using it to compute transitions.
         *
         * Returned transition may be a duplicate from previous
         * transition. Caller must call isTransitionFrom to filter out
         * duplicates.
         *
         * @param saveMillis savings before next transition
         */
        public Transition nextTransition(final long instant, final int saveMillis) {
            Chronology chrono = ISOChronology.getInstanceUTC();

            // Find next matching rule.
            Rule nextRule = null;
            long nextMillis = Long.MAX_VALUE;
            
            Iterator<Rule> it = iRules.iterator();
            while (it.hasNext()) {
                Rule rule = it.next();
                long next = rule.next(instant, iStandardOffset, saveMillis);
                if (next <= instant) {
                    it.remove();
                    continue;
                }
                // Even if next is same as previous next, choose the rule
                // in order for more recently added rules to override.
                if (next <= nextMillis) {
                    // Found a better match.
                    nextRule = rule;
                    nextMillis = next;
                }
            }
            
            if (nextRule == null) {
                return null;
            }
            
            // Stop precalculating if year reaches some arbitrary limit.
            if (chrono.year().get(nextMillis) >= YEAR_LIMIT) {
                return null;
            }
            
            // Check if upper limit reached or passed.
            if (iUpperYear < Integer.MAX_VALUE) {
                long upperMillis =
                    iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis);
                if (nextMillis >= upperMillis) {
                    // At or after upper limit.
                    return null;
                }
            }
            
            return new Transition(nextMillis, nextRule, iStandardOffset);
        }

        /**
         * @param saveMillis savings before upper limit
         */
        public long getUpperLimit(int saveMillis) {
            if (iUpperYear == Integer.MAX_VALUE) {
                return Long.MAX_VALUE;
            }
            return iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis);
        }

        /**
         * Returns null if none can be built.
         */
        public DSTZone buildTailZone(String id) {
            if (iRules.size() == 2) {
                Rule startRule = iRules.get(0);
                Rule endRule = iRules.get(1);
                if (startRule.getToYear() == Integer.MAX_VALUE &&
                    endRule.getToYear() == Integer.MAX_VALUE) {

                    // With exactly two infinitely recurring rules left, a
                    // simple DSTZone can be formed.

                    // The order of rules can come in any order, and it doesn't
                    // really matter which rule was chosen the 'start' and
                    // which is chosen the 'end'. DSTZone works properly either
                    // way.
                    return new DSTZone(id, iStandardOffset,
                                       startRule.iRecurrence, endRule.iRecurrence);
                }
            }
            return null;
        }
    }

    private static final class DSTZone extends DateTimeZone {
        private static final long serialVersionUID = 6941492635554961361L;

        static DSTZone readFrom(DataInput in, String id) throws IOException {
            return new DSTZone(id, (int)readMillis(in), 
                               Recurrence.readFrom(in), Recurrence.readFrom(in));
        }

        final int iStandardOffset;
        final Recurrence iStartRecurrence;
        final Recurrence iEndRecurrence;

        DSTZone(String id, int standardOffset,
                Recurrence startRecurrence, Recurrence endRecurrence) {
            super(id);
            iStandardOffset = standardOffset;
            iStartRecurrence = startRecurrence;
            iEndRecurrence = endRecurrence;
        }

        public String getNameKey(long instant) {
            return findMatchingRecurrence(instant).getNameKey();
        }

        public int getOffset(long instant) {
            return iStandardOffset + findMatchingRecurrence(instant).getSaveMillis();
        }

        public int getStandardOffset(long instant) {
            return iStandardOffset;
        }

        public boolean isFixed() {
            return false;
        }

        public long nextTransition(long instant) {
            int standardOffset = iStandardOffset;
            Recurrence startRecurrence = iStartRecurrence;
            Recurrence endRecurrence = iEndRecurrence;

            long start, end;

            try {
                start = startRecurrence.next
                    (instant, standardOffset, endRecurrence.getSaveMillis());
                if (instant > 0 && start < 0) {
                    // Overflowed.
                    start = instant;
                }
            } catch (IllegalArgumentException e) {
                // Overflowed.
                start = instant;
            } catch (ArithmeticException e) {
                // Overflowed.
                start = instant;
            }

            try {
                end = endRecurrence.next
                    (instant, standardOffset, startRecurrence.getSaveMillis());
                if (instant > 0 && end < 0) {
                    // Overflowed.
                    end = instant;
                }
            } catch (IllegalArgumentException e) {
                // Overflowed.
                end = instant;
            } catch (ArithmeticException e) {
                // Overflowed.
                end = instant;
            }

            return (start > end) ? end : start;
        }

        public long previousTransition(long instant) {
            // Increment in order to handle the case where instant is exactly at
            // a transition.
            instant++;

            int standardOffset = iStandardOffset;
            Recurrence startRecurrence = iStartRecurrence;
            Recurrence endRecurrence = iEndRecurrence;

            long start, end;

            try {
                start = startRecurrence.previous
                    (instant, standardOffset, endRecurrence.getSaveMillis());
                if (instant < 0 && start > 0) {
                    // Overflowed.
                    start = instant;
                }
            } catch (IllegalArgumentException e) {
                // Overflowed.
                start = instant;
            } catch (ArithmeticException e) {
                // Overflowed.
                start = instant;
            }

            try {
                end = endRecurrence.previous
                    (instant, standardOffset, startRecurrence.getSaveMillis());
                if (instant < 0 && end > 0) {
                    // Overflowed.
                    end = instant;
                }
            } catch (IllegalArgumentException e) {
                // Overflowed.
                end = instant;
            } catch (ArithmeticException e) {
                // Overflowed.
                end = instant;
            }

            return ((start > end) ? start : end) - 1;
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof DSTZone) {
                DSTZone other = (DSTZone)obj;
                return
                    getID().equals(other.getID()) &&
                    iStandardOffset == other.iStandardOffset &&
                    iStartRecurrence.equals(other.iStartRecurrence) &&
                    iEndRecurrence.equals(other.iEndRecurrence);
            }
            return false;
        }

        public void writeTo(DataOutput out) throws IOException {
            writeMillis(out, iStandardOffset);
            iStartRecurrence.writeTo(out);
            iEndRecurrence.writeTo(out);
        }

        private Recurrence findMatchingRecurrence(long instant) {
            int standardOffset = iStandardOffset;
            Recurrence startRecurrence = iStartRecurrence;
            Recurrence endRecurrence = iEndRecurrence;

            long start, end;

            try {
                start = startRecurrence.next
                    (instant, standardOffset, endRecurrence.getSaveMillis());
            } catch (IllegalArgumentException e) {
                // Overflowed.
                start = instant;
            } catch (ArithmeticException e) {
                // Overflowed.
                start = instant;
            }

            try {
                end = endRecurrence.next
                    (instant, standardOffset, startRecurrence.getSaveMillis());
            } catch (IllegalArgumentException e) {
                // Overflowed.
                end = instant;
            } catch (ArithmeticException e) {
                // Overflowed.
                end = instant;
            }

            return (start > end) ? startRecurrence : endRecurrence;
        }
    }

    private static final class PrecalculatedZone extends DateTimeZone {
        private static final long serialVersionUID = 7811976468055766265L;

        static PrecalculatedZone readFrom(DataInput in, String id) throws IOException {
            // Read string pool.
            int poolSize = in.readUnsignedShort();
            String[] pool = new String[poolSize];
            for (int i=0; i<poolSize; i++) {
                pool[i] = in.readUTF();
            }

            int size = in.readInt();
            long[] transitions = new long[size];
            int[] wallOffsets = new int[size];
            int[] standardOffsets = new int[size];
            String[] nameKeys = new String[size];
            
            for (int i=0; i<size; i++) {
                transitions[i] = readMillis(in);
                wallOffsets[i] = (int)readMillis(in);
                standardOffsets[i] = (int)readMillis(in);
                try {
                    int index;
                    if (poolSize < 256) {
                        index = in.readUnsignedByte();
                    } else {
                        index = in.readUnsignedShort();
                    }
                    nameKeys[i] = pool[index];
                } catch (ArrayIndexOutOfBoundsException e) {
                    throw new IOException("Invalid encoding");
                }
            }

            DSTZone tailZone = null;
            if (in.readBoolean()) {
                tailZone = DSTZone.readFrom(in, id);
            }

            return new PrecalculatedZone
                (id, transitions, wallOffsets, standardOffsets, nameKeys, tailZone);
        }

        /**
         * Factory to create instance from builder.
         * 
         * @param id  the zone id
         * @param outputID  true if the zone id should be output
         * @param transitions  the list of Transition objects
         * @param tailZone  optional zone for getting info beyond precalculated tables
         */
        static PrecalculatedZone create(String id, boolean outputID, ArrayList<Transition> transitions,
                                        DSTZone tailZone) {
            int size = transitions.size();
            if (size == 0) {
                throw new IllegalArgumentException();
            }

            long[] trans = new long[size];
            int[] wallOffsets = new int[size];
            int[] standardOffsets = new int[size];
            String[] nameKeys = new String[size];

            Transition last = null;
            for (int i=0; i<size; i++) {
                Transition tr = transitions.get(i);

                if (!tr.isTransitionFrom(last)) {
                    throw new IllegalArgumentException(id);
                }

                trans[i] = tr.getMillis();
                wallOffsets[i] = tr.getWallOffset();
                standardOffsets[i] = tr.getStandardOffset();
                nameKeys[i] = tr.getNameKey();

                last = tr;
            }

            // Some timezones (Australia) have the same name key for
            // summer and winter which messes everything up. Fix it here.
            String[] zoneNameData = new String[5];
            String[][] zoneStrings = new DateFormatSymbols(Locale.ENGLISH).getZoneStrings();
            for (int j = 0; j < zoneStrings.length; j++) {
                String[] set = zoneStrings[j];
                if (set != null && set.length == 5 && id.equals(set[0])) {
                    zoneNameData = set;
                }
            }

            Chronology chrono = ISOChronology.getInstanceUTC();

            for (int i = 0; i < nameKeys.length - 1; i++) {
                String curNameKey = nameKeys[i];
                String nextNameKey = nameKeys[i + 1];
                long curOffset = wallOffsets[i];
                long nextOffset = wallOffsets[i + 1];
                long curStdOffset = standardOffsets[i];
                long nextStdOffset = standardOffsets[i + 1];
                Period p = new Period(trans[i], trans[i + 1], PeriodType.yearMonthDay(), chrono);
                if (curOffset != nextOffset &&
                        curStdOffset == nextStdOffset &&
                        curNameKey.equals(nextNameKey) &&
                        p.getYears() == 0 && p.getMonths() > 4 && p.getMonths() < 8 &&
                        curNameKey.equals(zoneNameData[2]) &&
                        curNameKey.equals(zoneNameData[4])) {
                    
                    if (ZoneInfoLogger.verbose()) {
                        System.out.println("Fixing duplicate name key - " + nextNameKey);
                        System.out.println("     - " + new DateTime(trans[i], chrono) +
                                           " - " + new DateTime(trans[i + 1], chrono));
                    }
                    if (curOffset > nextOffset) {
                        nameKeys[i] = (curNameKey + "-Summer").intern();
                    } else if (curOffset < nextOffset) {
                        nameKeys[i + 1] = (nextNameKey + "-Summer").intern();
                        i++;
                    }
                }
            }

            if (tailZone != null) {
                if (tailZone.iStartRecurrence.getNameKey()
                    .equals(tailZone.iEndRecurrence.getNameKey())) {
                    if (ZoneInfoLogger.verbose()) {
                        System.out.println("Fixing duplicate recurrent name key - " +
                                           tailZone.iStartRecurrence.getNameKey());
                    }
                    if (tailZone.iStartRecurrence.getSaveMillis() > 0) {
                        tailZone = new DSTZone(
                            tailZone.getID(),
                            tailZone.iStandardOffset,
                            tailZone.iStartRecurrence.renameAppend("-Summer"),
                            tailZone.iEndRecurrence);
                    } else {
                        tailZone = new DSTZone(
                            tailZone.getID(),
                            tailZone.iStandardOffset,
                            tailZone.iStartRecurrence,
                            tailZone.iEndRecurrence.renameAppend("-Summer"));
                    }
                }
            }
            
            return new PrecalculatedZone
                ((outputID ? id : ""), trans, wallOffsets, standardOffsets, nameKeys, tailZone);
        }

        // All array fields have the same length.

        private final long[] iTransitions;

        private final int[] iWallOffsets;
        private final int[] iStandardOffsets;
        private final String[] iNameKeys;

        private final DSTZone iTailZone;

        /**
         * Constructor used ONLY for valid input, loaded via static methods.
         */
        private PrecalculatedZone(String id, long[] transitions, int[] wallOffsets,
                          int[] standardOffsets, String[] nameKeys, DSTZone tailZone)
        {
            super(id);
            iTransitions = transitions;
            iWallOffsets = wallOffsets;
            iStandardOffsets = standardOffsets;
            iNameKeys = nameKeys;
            iTailZone = tailZone;
        }

        public String getNameKey(long instant) {
            long[] transitions = iTransitions;
            int i = Arrays.binarySearch(transitions, instant);
            if (i >= 0) {
                return iNameKeys[i];
            }
            i = ~i;
            if (i < transitions.length) {
                if (i > 0) {
                    return iNameKeys[i - 1];
                }
                return "UTC";
            }
            if (iTailZone == null) {
                return iNameKeys[i - 1];
            }
            return iTailZone.getNameKey(instant);
        }

        public int getOffset(long instant) {
            long[] transitions = iTransitions;
            int i = Arrays.binarySearch(transitions, instant);
            if (i >= 0) {
                return iWallOffsets[i];
            }
            i = ~i;
            if (i < transitions.length) {
                if (i > 0) {
                    return iWallOffsets[i - 1];
                }
                return 0;
            }
            if (iTailZone == null) {
                return iWallOffsets[i - 1];
            }
            return iTailZone.getOffset(instant);
        }

        public int getStandardOffset(long instant) {
            long[] transitions = iTransitions;
            int i = Arrays.binarySearch(transitions, instant);
            if (i >= 0) {
                return iStandardOffsets[i];
            }
            i = ~i;
            if (i < transitions.length) {
                if (i > 0) {
                    return iStandardOffsets[i - 1];
                }
                return 0;
            }
            if (iTailZone == null) {
                return iStandardOffsets[i - 1];
            }
            return iTailZone.getStandardOffset(instant);
        }

        public boolean isFixed() {
            return false;
        }

        public long nextTransition(long instant) {
            long[] transitions = iTransitions;
            int i = Arrays.binarySearch(transitions, instant);
            i = (i >= 0) ? (i + 1) : ~i;
            if (i < transitions.length) {
                return transitions[i];
            }
            if (iTailZone == null) {
                return instant;
            }
            long end = transitions[transitions.length - 1];
            if (instant < end) {
                instant = end;
            }
            return iTailZone.nextTransition(instant);
        }

        public long previousTransition(long instant) {
            long[] transitions = iTransitions;
            int i = Arrays.binarySearch(transitions, instant);
            if (i >= 0) {
                if (instant > Long.MIN_VALUE) {
                    return instant - 1;
                }
                return instant;
            }
            i = ~i;
            if (i < transitions.length) {
                if (i > 0) {
                    long prev = transitions[i - 1];
                    if (prev > Long.MIN_VALUE) {
                        return prev - 1;
                    }
                }
                return instant;
            }
            if (iTailZone != null) {
                long prev = iTailZone.previousTransition(instant);
                if (prev < instant) {
                    return prev;
                }
            }
            long prev = transitions[i - 1];
            if (prev > Long.MIN_VALUE) {
                return prev - 1;
            }
            return instant;
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof PrecalculatedZone) {
                PrecalculatedZone other = (PrecalculatedZone)obj;
                return
                    getID().equals(other.getID()) &&
                    Arrays.equals(iTransitions, other.iTransitions) &&
                    Arrays.equals(iNameKeys, other.iNameKeys) &&
                    Arrays.equals(iWallOffsets, other.iWallOffsets) &&
                    Arrays.equals(iStandardOffsets, other.iStandardOffsets) &&
                    ((iTailZone == null)
                     ? (null == other.iTailZone)
                     : (iTailZone.equals(other.iTailZone)));
            }
            return false;
        }

        public void writeTo(DataOutput out) throws IOException {
            int size = iTransitions.length;

            // Create unique string pool.
            Set<String> poolSet = new HashSet();
            for (int i=0; i<size; i++) {
                poolSet.add(iNameKeys[i]);
            }

            int poolSize = poolSet.size();
            if (poolSize > 65535) {
                throw new UnsupportedOperationException("String pool is too large");
            }
            String[] pool = new String[poolSize];
            Iterator<String> it = poolSet.iterator();
            for (int i=0; it.hasNext(); i++) {
                pool[i] = it.next();
            }

            // Write out the pool.
            out.writeShort(poolSize);
            for (int i=0; i<poolSize; i++) {
                out.writeUTF(pool[i]);
            }

            out.writeInt(size);

            for (int i=0; i<size; i++) {
                writeMillis(out, iTransitions[i]);
                writeMillis(out, iWallOffsets[i]);
                writeMillis(out, iStandardOffsets[i]);
                
                // Find pool index and write it out.
                String nameKey = iNameKeys[i];
                for (int j=0; j<poolSize; j++) {
                    if (pool[j].equals(nameKey)) {
                        if (poolSize < 256) {
                            out.writeByte(j);
                        } else {
                            out.writeShort(j);
                        }
                        break;
                    }
                }
            }

            out.writeBoolean(iTailZone != null);
            if (iTailZone != null) {
                iTailZone.writeTo(out);
            }
        }

        public boolean isCachable() {
            if (iTailZone != null) {
                return true;
            }
            long[] transitions = iTransitions;
            if (transitions.length <= 1) {
                return false;
            }

            // Add up all the distances between transitions that are less than
            // about two years.
            double distances = 0;
            int count = 0;

            for (int i=1; i<transitions.length; i++) {
                long diff = transitions[i] - transitions[i - 1];
                if (diff < ((366L + 365) * 24 * 60 * 60 * 1000)) {
                    distances += (double)diff;
                    count++;
                }
            }

            if (count > 0) {
                double avg = distances / count;
                avg /= 24 * 60 * 60 * 1000;
                if (avg >= 25) {
                    // Only bother caching if average distance between
                    // transitions is at least 25 days. Why 25?
                    // CachedDateTimeZone is more efficient if the distance
                    // between transitions is large. With an average of 25, it
                    // will on average perform about 2 tests per cache
                    // hit. (49.7 / 25) is approximately 2.
                    return true;
                }
            }

            return false;
        }
    }
}

Other Java examples (source code examples)

Here is a short list of links related to this Java DateTimeZoneBuilder.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.