Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
   /*
    *  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.util.Arrays;
  import java.util.HashSet;
  import java.util.Locale;
  import java.util.Set;
  
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.

DateTimeZoneBuilder itself is mutable and not thread-safe, but the DateTimeZone objects that it builds are thread-safe and immutable.

It is intended that 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:

 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);
 

Author(s):
Brian S O'Neill
Since:
1.0
See also:
ZoneInfoCompiler
ZoneInfoProvider
  
  public class DateTimeZoneBuilder {
    
Decodes a built DateTimeZone from the given stream, as encoded by writeTo.

Parameters:
in input stream to read encoded DateTimeZone from.
id time zone id to assign
  
      public static DateTimeZone readFrom(InputStream inString idthrows IOException {
          if (in instanceof DataInput) {
              return readFrom((DataInput)inid);
          } else {
              return readFrom((DataInput)new DataInputStream(in), id);
          }
     }

    
Decodes a built DateTimeZone from the given stream, as encoded by writeTo.

Parameters:
in input stream to read encoded DateTimeZone from.
id time zone id to assign
 
     public static DateTimeZone readFrom(DataInput inString idthrows IOException {
         switch (in.readUnsignedByte()) {
         case 'F':
             DateTimeZone fixed = new FixedDateTimeZone
                 (idin.readUTF(), (int)readMillis(in), (int)readMillis(in));
             if (fixed.equals(.)) {
                 fixed = .;
             }
             return fixed;
         case 'C':
             return CachedDateTimeZone.forZone(PrecalculatedZone.readFrom(inid));
         case 'P':
             return PrecalculatedZone.readFrom(inid);
         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 outlong millisthrows 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 inthrows 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 idString nameKey,
                                                int wallOffsetint standardOffset) {
         if ("UTC".equals(id) && id.equals(nameKey) &&
             wallOffset == 0 && standardOffset == 0) {
             return .;
         }
         return new FixedDateTimeZone(idnameKeywallOffsetstandardOffset);
     }
 
     // List of RuleSets.
     private final ArrayList<RuleSetiRuleSets;
 
     public DateTimeZoneBuilder() {
          = 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.

Parameters:
year the year of cutover
mode 'u' - cutover is measured against UTC, 'w' - against wall offset, 's' - against standard offset
monthOfYear the month from 1 (January) to 12 (December)
dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). For example, if -1, set to last day of month
dayOfWeek from 1 (Monday) to 7 (Sunday), if 0 then ignore
advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to dayOfWeek when true, retreat when false.
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 (.size() > 0) {
             OfYear ofYear = new OfYear
                 (modemonthOfYeardayOfMonthdayOfWeekadvanceDayOfWeekmillisOfDay);
             RuleSet lastRuleSet = .get(.size() - 1);
             lastRuleSet.setUpperLimit(yearofYear);
         }
         .add(new RuleSet());
         return this;
     }

    
Sets the standard offset to use for newly added rules until the next cutover is added.

Parameters:
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 nameKeyint saveMillis) {
         getLastRuleSet().setFixedSavings(nameKeysaveMillis);
         return this;
     }

    
Add a recurring daylight saving time rule.

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

Parameters:
id time zone id to assign
outputID true if the zone id should be output
 
     public DateTimeZone toDateTimeZone(String idboolean outputID) {
         if (id == null) {
             throw new IllegalArgumentException();
         }
 
         // Discover where all the transitions occur and store the results in
         // these lists.
         ArrayList<Transitiontransitions = new ArrayList<Transition>();
 
         // Tail zone picks up remaining transitions in the form of an endless
         // DST cycle.
         DSTZone tailZone = null;
 
         long millis = .;
         int saveMillis = 0;
             
         int ruleSetCount = .size();
         for (int i=0; i<ruleSetCounti++) {
             RuleSet rs = .get(i);
             Transition next = rs.firstTransition(millis);
             if (next == null) {
                 continue;
             }
             addTransition(transitionsnext);
             millis = next.getMillis();
             saveMillis = next.getSaveMillis();
 
             // Copy it since we're going to destroy it.
             rs = new RuleSet(rs);
 
             while ((next = rs.nextTransition(millissaveMillis)) != null) {
                 if (addTransition(transitionsnext)) {
                     if (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(idtr.getNameKey(),
                                   tr.getWallOffset(), tr.getStandardOffset());
         }
 
         PrecalculatedZone zone = PrecalculatedZone.create(idoutputIDtransitionstailZone);
         if (zone.isCachable()) {
             return CachedDateTimeZone.forZone(zone);
         }
         return zone;
     }
 
     private boolean addTransition(ArrayList<TransitiontransitionsTransition 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(transitionstr);
     }

    
Encodes a built DateTimeZone to the given stream. Call readFrom to decode the data into a DateTimeZone object.

Parameters:
out the output stream to receive the encoded DateTimeZone
Since:
1.5 (parameter added)
 
     public void writeTo(String zoneIDOutputStream outthrows IOException {
         if (out instanceof DataOutput) {
             writeTo(zoneID, (DataOutput)out);
         } else {
             writeTo(zoneID, (DataOutput)new DataOutputStream(out));
         }
     }

    
Encodes a built DateTimeZone to the given stream. Call readFrom to decode the data into a DateTimeZone object.

Parameters:
out the output stream to receive the encoded DateTimeZone
Since:
1.5 (parameter added)
 
     public void writeTo(String zoneIDDataOutput outthrows IOException {
         // pass false so zone id is not written out
         DateTimeZone zone = toDateTimeZone(zoneIDfalse);
 
         if (zone instanceof FixedDateTimeZone) {
             out.writeByte('F'); // 'F' for fixed
             out.writeUTF(zone.getNameKey(0));
             writeMillis(outzone.getOffset(0));
             writeMillis(outzone.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 inthrows 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 dayOfWeekboolean advanceDayOfWeek,
                int millisOfDay)
         {
             if (mode != 'u' && mode != 'w' && mode != 's') {
                 throw new IllegalArgumentException("Unknown mode: " + mode);
             }
 
              = mode;
              = monthOfYear;
              = dayOfMonth;
              = dayOfWeek;
              = advanceDayOfWeek;
              = millisOfDay;
         }

        

Parameters:
standardOffset standard offset just before instant
 
         public long setInstant(int yearint standardOffsetint saveMillis) {
             int offset;
             if ( == 'w') {
                 offset = standardOffset + saveMillis;
             } else if ( == 's') {
                 offset = standardOffset;
             } else {
                 offset = 0;
             }
 
             Chronology chrono = ISOChronology.getInstanceUTC();
             long millis = chrono.year().set(0, year);
             millis = chrono.monthOfYear().set(millis);
             millis = chrono.millisOfDay().set(millis);
             millis = setDayOfMonth(chronomillis);
 
             if ( != 0) {
                 millis = setDayOfWeek(chronomillis);
             }
 
             // Convert from local time to UTC.
             return millis - offset;
         }

        

Parameters:
standardOffset standard offset just before next recurrence
 
         public long next(long instantint standardOffsetint saveMillis) {
             int offset;
             if ( == 'w') {
                 offset = standardOffset + saveMillis;
             } else if ( == 's') {
                 offset = standardOffset;
             } else {
                 offset = 0;
             }
 
             // Convert from UTC to local time.
             instant += offset;
 
             Chronology chrono = ISOChronology.getInstanceUTC();
             long next = chrono.monthOfYear().set(instant);
             // Be lenient with millisOfDay.
             next = chrono.millisOfDay().set(next, 0);
             next = chrono.millisOfDay().add(next);
             next = setDayOfMonthNext(chrononext);
 
             if ( == 0) {
                 if (next <= instant) {
                     next = chrono.year().add(next, 1);
                     next = setDayOfMonthNext(chrononext);
                 }
             } else {
                 next = setDayOfWeek(chrononext);
                 if (next <= instant) {
                     next = chrono.year().add(next, 1);
                     next = chrono.monthOfYear().set(next);
                     next = setDayOfMonthNext(chrononext);
                     next = setDayOfWeek(chrononext);
                 }
             }
 
             // Convert from local time to UTC.
             return next - offset;
         }

        

Parameters:
standardOffset standard offset just before previous recurrence
 
         public long previous(long instantint standardOffsetint saveMillis) {
             int offset;
             if ( == 'w') {
                 offset = standardOffset + saveMillis;
             } else if ( == 's') {
                 offset = standardOffset;
             } else {
                 offset = 0;
             }
 
             // Convert from UTC to local time.
             instant += offset;
 
             Chronology chrono = ISOChronology.getInstanceUTC();
             long prev = chrono.monthOfYear().set(instant);
             // Be lenient with millisOfDay.
             prev = chrono.millisOfDay().set(prev, 0);
             prev = chrono.millisOfDay().add(prev);
             prev = setDayOfMonthPrevious(chronoprev);
 
             if ( == 0) {
                 if (prev >= instant) {
                     prev = chrono.year().add(prev, -1);
                     prev = setDayOfMonthPrevious(chronoprev);
                 }
             } else {
                 prev = setDayOfWeek(chronoprev);
                 if (prev >= instant) {
                     prev = chrono.year().add(prev, -1);
                     prev = chrono.monthOfYear().set(prev);
                     prev = setDayOfMonthPrevious(chronoprev);
                     prev = setDayOfWeek(chronoprev);
                 }
             }
 
             // 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
                      == other.iMode &&
                      == other.iMonthOfYear &&
                      == other.iDayOfMonth &&
                      == other.iDayOfWeek &&
                      == other.iAdvance &&
                      == 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 outthrows IOException {
             out.writeByte();
             out.writeByte();
             out.writeByte();
             out.writeByte();
             out.writeBoolean();
             writeMillis(out);
         }

        
If month-day is 02-29 and year isn't leap, advances to next leap year.
 
         private long setDayOfMonthNext(Chronology chronolong next) {
             try {
                 next = setDayOfMonth(chrononext);
             } catch (IllegalArgumentException e) {
                 if ( == 2 &&  == 29) {
                     while (chrono.year().isLeap(next) == false) {
                         next = chrono.year().add(next, 1);
                     }
                     next = setDayOfMonth(chrononext);
                 } 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 chronolong prev) {
             try {
                 prev = setDayOfMonth(chronoprev);
             } catch (IllegalArgumentException e) {
                 if ( == 2 &&  == 29) {
                     while (chrono.year().isLeap(prev) == false) {
                         prev = chrono.year().add(prev, -1);
                     }
                     prev = setDayOfMonth(chronoprev);
                 } else {
                     throw e;
                 }
             }
             return prev;
         }
 
         private long setDayOfMonth(Chronology chronolong instant) {
             if ( >= 0) {
                 instant = chrono.dayOfMonth().set(instant);
             } else {
                 instant = chrono.dayOfMonth().set(instant, 1);
                 instant = chrono.monthOfYear().add(instant, 1);
                 instant = chrono.dayOfMonth().add(instant);
             }
             return instant;
         }
 
         private long setDayOfWeek(Chronology chronolong instant) {
             int dayOfWeek = chrono.dayOfWeek().get(instant);
             int daysToAdd =  - dayOfWeek;
             if (daysToAdd != 0) {
                 if () {
                     if (daysToAdd < 0) {
                         daysToAdd += 7;
                     }
                 } else {
                     if (daysToAdd > 0) {
                         daysToAdd -= 7;
                     }
                 }
                 instant = chrono.dayOfWeek().add(instantdaysToAdd);
             }
             return instant;
         }
     }

    
Extends OfYear with a nameKey and savings.
 
     private static final class Recurrence {
         static Recurrence readFrom(DataInput inthrows IOException {
             return new Recurrence(OfYear.readFrom(in), in.readUTF(), (int)readMillis(in));
         }
 
         final OfYear iOfYear;
         final String iNameKey;
         final int iSaveMillis;
 
         Recurrence(OfYear ofYearString nameKeyint saveMillis) {
              = ofYear;
              = nameKey;
              = saveMillis;
         }
 
         public OfYear getOfYear() {
             return ;
         }

        

Parameters:
standardOffset standard offset just before next recurrence
 
         public long next(long instantint standardOffsetint saveMillis) {
             return .next(instantstandardOffsetsaveMillis);
         }

        

Parameters:
standardOffset standard offset just before previous recurrence
 
         public long previous(long instantint standardOffsetint saveMillis) {
             return .previous(instantstandardOffsetsaveMillis);
         }
 
         public String getNameKey() {
             return ;
         }
 
         public int getSaveMillis() {
             return ;
         }
 
         public boolean equals(Object obj) {
             if (this == obj) {
                 return true;
             }
             if (obj instanceof Recurrence) {
                 Recurrence other = (Recurrence)obj;
                 return
                      == other.iSaveMillis &&
                     .equals(other.iNameKey) &&
                     .equals(other.iOfYear);
             }
             return false;
         }
 
         public void writeTo(DataOutput outthrows IOException {
             .writeTo(out);
             out.writeUTF();
             writeMillis(out);
         }
 
         Recurrence rename(String nameKey) {
             return new Recurrence(nameKey);
         }
 
         Recurrence renameAppend(String appendNameKey) {
             return rename(( + 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 recurrenceint fromYearint toYear) {
              = recurrence;
              = fromYear;
              = toYear;
         }
 
         @SuppressWarnings("unused")
         public int getFromYear() {
             return ;
         }
 
         public int getToYear() {
             return ;
         }
 
         @SuppressWarnings("unused")
         public OfYear getOfYear() {
             return .getOfYear();
         }
 
         public String getNameKey() {
             return .getNameKey();
         }
 
         public int getSaveMillis() {
             return .getSaveMillis();
         }
 
         public long next(final long instantint standardOffsetint saveMillis) {
             Chronology chrono = ISOChronology.getInstanceUTC();
 
             final int wallOffset = standardOffset + saveMillis;
             long testInstant = instant;
 
             int year;
             if (instant == .) {
                 year = .;
             } else {
                 year = chrono.year().get(instant + wallOffset);
             }
 
             if (year < ) {
                 // First advance instant to start of from year.
                 testInstant = chrono.year().set(0, ) - wallOffset;
                 // Back off one millisecond to account for next recurrence
                 // being exactly at the beginning of the year.
                 testInstant -= 1;
             }
 
             long next = .next(testInstantstandardOffsetsaveMillis);
 
             if (next > instant) {
                 year = chrono.year().get(next + wallOffset);
                 if (year > ) {
                     // 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 millisTransition tr) {
              = millis;
              = tr.iNameKey;
              = tr.iWallOffset;
              = tr.iStandardOffset;
         }
 
         Transition(long millisRule ruleint standardOffset) {
              = millis;
              = rule.getNameKey();
              = standardOffset + rule.getSaveMillis();
              = standardOffset;
         }
 
         Transition(long millisString nameKey,
                    int wallOffsetint standardOffset) {
              = millis;
              = nameKey;
              = wallOffset;
              = standardOffset;
         }
 
         public long getMillis() {
             return ;
         }
 
         public String getNameKey() {
             return ;
         }
 
         public int getWallOffset() {
             return ;
         }
 
         public int getStandardOffset() {
             return ;
         }
 
         public int getSaveMillis() {
             return  - ;
         }

        
There must be a change in the millis, wall offsets or name keys.
 
         public boolean isTransitionFrom(Transition other) {
             if (other == null) {
                 return true;
             }
             return  > other.iMillis &&
                 ( != other.iWallOffset ||
                  //iStandardOffset != other.iStandardOffset ||
                  !(.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();
              = ISOChronology.getInstanceUTC().year().get(now) + 100;
         }
 
         private int iStandardOffset;
         private ArrayList<RuleiRules;
 
         // Optional.
         private String iInitialNameKey;
         private int iInitialSaveMillis;
 
         // Upper limit is exclusive.
         private int iUpperYear;
         private OfYear iUpperOfYear;
 
         RuleSet() {
              = new ArrayList<Rule>(10);
              = .;
         }

        
Copy constructor.
 
         RuleSet(RuleSet rs) {
              = rs.iStandardOffset;
              = new ArrayList<Rule>(rs.iRules);
              = rs.iInitialNameKey;
              = rs.iInitialSaveMillis;
              = rs.iUpperYear;
              = rs.iUpperOfYear;
         }
 
         @SuppressWarnings("unused")
         public int getStandardOffset() {
             return ;
         }
 
         public void setStandardOffset(int standardOffset) {
              = standardOffset;
         }
 
         public void setFixedSavings(String nameKeyint saveMillis) {
              = nameKey;
              = saveMillis;
         }
 
         public void addRule(Rule rule) {
             if (!.contains(rule)) {
                 .add(rule);
             }
        }
        public void setUpperLimit(int yearOfYear ofYear) {
             = year;
             = ofYear;
        }

        
Returns a transition at firstMillis with the first name key and offsets for this rule set. This method may return null.

Parameters:
firstMillis millis of first transition
        public Transition firstTransition(final long firstMillis) {
            if ( != null) {
                // Initial zone info explicitly set, so don't search the rules.
                return new Transition(firstMillis,
                                       + );
            }
            // Make a copy before we destroy the rules.
            ArrayList<Rulecopy = new ArrayList<Rule>();
            // Iterate through all the transitions until firstMillis is
            // reached. Use the name key and savings for whatever rule reaches
            // the limit.
            long millis = .;
            int saveMillis = 0;
            Transition first = null;
            Transition next;
            while ((next = nextTransition(millissaveMillis)) != null) {
                millis = next.getMillis();
                if (millis == firstMillis) {
                    first = new Transition(firstMillisnext);
                    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(firstMillisrule);
                                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(firstMillisnext.getNameKey(),
                                               );
                    }
                    break;
                }
                
                // Set first to the best transition found so far, but next
                // iteration may find something closer to lower limit.
                first = new Transition(firstMillisnext);
                saveMillis = next.getSaveMillis();
            }
             = 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.

Parameters:
saveMillis savings before next transition
        public Transition nextTransition(final long instantfinal int saveMillis) {
            Chronology chrono = ISOChronology.getInstanceUTC();
            // Find next matching rule.
            Rule nextRule = null;
            long nextMillis = .;
            
            Iterator<Ruleit = .iterator();
            while (it.hasNext()) {
                Rule rule = it.next();
                long next = rule.next(instantsaveMillis);
                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) >= ) {
                return null;
            }
            
            // Check if upper limit reached or passed.
            if ( < .) {
                long upperMillis =
                    .setInstant(saveMillis);
                if (nextMillis >= upperMillis) {
                    // At or after upper limit.
                    return null;
                }
            }
            
            return new Transition(nextMillisnextRule);
        }

        

Parameters:
saveMillis savings before upper limit
        public long getUpperLimit(int saveMillis) {
            if ( == .) {
                return .;
            }
            return .setInstant(saveMillis);
        }

        
Returns null if none can be built.
        public DSTZone buildTailZone(String id) {
            if (.size() == 2) {
                Rule startRule = .get(0);
                Rule endRule = .get(1);
                if (startRule.getToYear() == . &&
                    endRule.getToYear() == .) {
                    // 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,
                                       startRule.iRecurrenceendRule.iRecurrence);
                }
            }
            return null;
        }
    }
    private static final class DSTZone extends DateTimeZone {
        private static final long serialVersionUID = 6941492635554961361L;
        static DSTZone readFrom(DataInput inString idthrows 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 idint standardOffset,
                Recurrence startRecurrenceRecurrence endRecurrence) {
            super(id);
             = standardOffset;
             = startRecurrence;
             = endRecurrence;
        }
        public String getNameKey(long instant) {
            return findMatchingRecurrence(instant).getNameKey();
        }
        public int getOffset(long instant) {
            return  + findMatchingRecurrence(instant).getSaveMillis();
        }
        public int getStandardOffset(long instant) {
            return ;
        }
        public boolean isFixed() {
            return false;
        }
        public long nextTransition(long instant) {
            int standardOffset = ;