0001: /*
0002: * Copyright 2001-2005 Stephen Colebourne
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016: package org.joda.time.tz;
0017:
0018: import java.io.DataInput;
0019: import java.io.DataInputStream;
0020: import java.io.DataOutput;
0021: import java.io.DataOutputStream;
0022: import java.io.IOException;
0023: import java.io.InputStream;
0024: import java.io.OutputStream;
0025: import java.text.DateFormatSymbols;
0026: import java.util.ArrayList;
0027: import java.util.Arrays;
0028: import java.util.HashSet;
0029: import java.util.Iterator;
0030: import java.util.Locale;
0031: import java.util.Set;
0032:
0033: import org.joda.time.Chronology;
0034: import org.joda.time.DateTime;
0035: import org.joda.time.DateTimeUtils;
0036: import org.joda.time.DateTimeZone;
0037: import org.joda.time.Period;
0038: import org.joda.time.PeriodType;
0039: import org.joda.time.chrono.ISOChronology;
0040:
0041: /**
0042: * DateTimeZoneBuilder allows complex DateTimeZones to be constructed. Since
0043: * creating a new DateTimeZone this way is a relatively expensive operation,
0044: * built zones can be written to a file. Reading back the encoded data is a
0045: * quick operation.
0046: * <p>
0047: * DateTimeZoneBuilder itself is mutable and not thread-safe, but the
0048: * DateTimeZone objects that it builds are thread-safe and immutable.
0049: * <p>
0050: * It is intended that {@link ZoneInfoCompiler} be used to read time zone data
0051: * files, indirectly calling DateTimeZoneBuilder. The following complex
0052: * example defines the America/Los_Angeles time zone, with all historical
0053: * transitions:
0054: *
0055: * <pre>
0056: * DateTimeZone America_Los_Angeles = new DateTimeZoneBuilder()
0057: * .addCutover(-2147483648, 'w', 1, 1, 0, false, 0)
0058: * .setStandardOffset(-28378000)
0059: * .setFixedSavings("LMT", 0)
0060: * .addCutover(1883, 'w', 11, 18, 0, false, 43200000)
0061: * .setStandardOffset(-28800000)
0062: * .addRecurringSavings("PDT", 3600000, 1918, 1919, 'w', 3, -1, 7, false, 7200000)
0063: * .addRecurringSavings("PST", 0, 1918, 1919, 'w', 10, -1, 7, false, 7200000)
0064: * .addRecurringSavings("PWT", 3600000, 1942, 1942, 'w', 2, 9, 0, false, 7200000)
0065: * .addRecurringSavings("PPT", 3600000, 1945, 1945, 'u', 8, 14, 0, false, 82800000)
0066: * .addRecurringSavings("PST", 0, 1945, 1945, 'w', 9, 30, 0, false, 7200000)
0067: * .addRecurringSavings("PDT", 3600000, 1948, 1948, 'w', 3, 14, 0, false, 7200000)
0068: * .addRecurringSavings("PST", 0, 1949, 1949, 'w', 1, 1, 0, false, 7200000)
0069: * .addRecurringSavings("PDT", 3600000, 1950, 1966, 'w', 4, -1, 7, false, 7200000)
0070: * .addRecurringSavings("PST", 0, 1950, 1961, 'w', 9, -1, 7, false, 7200000)
0071: * .addRecurringSavings("PST", 0, 1962, 1966, 'w', 10, -1, 7, false, 7200000)
0072: * .addRecurringSavings("PST", 0, 1967, 2147483647, 'w', 10, -1, 7, false, 7200000)
0073: * .addRecurringSavings("PDT", 3600000, 1967, 1973, 'w', 4, -1, 7, false, 7200000)
0074: * .addRecurringSavings("PDT", 3600000, 1974, 1974, 'w', 1, 6, 0, false, 7200000)
0075: * .addRecurringSavings("PDT", 3600000, 1975, 1975, 'w', 2, 23, 0, false, 7200000)
0076: * .addRecurringSavings("PDT", 3600000, 1976, 1986, 'w', 4, -1, 7, false, 7200000)
0077: * .addRecurringSavings("PDT", 3600000, 1987, 2147483647, 'w', 4, 1, 7, true, 7200000)
0078: * .toDateTimeZone("America/Los_Angeles");
0079: * </pre>
0080: *
0081: * @author Brian S O'Neill
0082: * @see ZoneInfoCompiler
0083: * @see ZoneInfoProvider
0084: * @since 1.0
0085: */
0086: public class DateTimeZoneBuilder {
0087: /**
0088: * Decodes a built DateTimeZone from the given stream, as encoded by
0089: * writeTo.
0090: *
0091: * @param in input stream to read encoded DateTimeZone from.
0092: * @param id time zone id to assign
0093: */
0094: public static DateTimeZone readFrom(InputStream in, String id)
0095: throws IOException {
0096: if (in instanceof DataInput) {
0097: return readFrom((DataInput) in, id);
0098: } else {
0099: return readFrom((DataInput) new DataInputStream(in), id);
0100: }
0101: }
0102:
0103: /**
0104: * Decodes a built DateTimeZone from the given stream, as encoded by
0105: * writeTo.
0106: *
0107: * @param in input stream to read encoded DateTimeZone from.
0108: * @param id time zone id to assign
0109: */
0110: public static DateTimeZone readFrom(DataInput in, String id)
0111: throws IOException {
0112: switch (in.readUnsignedByte()) {
0113: case 'F':
0114: DateTimeZone fixed = new FixedDateTimeZone(id,
0115: in.readUTF(), (int) readMillis(in),
0116: (int) readMillis(in));
0117: if (fixed.equals(DateTimeZone.UTC)) {
0118: fixed = DateTimeZone.UTC;
0119: }
0120: return fixed;
0121: case 'C':
0122: return CachedDateTimeZone.forZone(PrecalculatedZone
0123: .readFrom(in, id));
0124: case 'P':
0125: return PrecalculatedZone.readFrom(in, id);
0126: default:
0127: throw new IOException("Invalid encoding");
0128: }
0129: }
0130:
0131: /**
0132: * Millisecond encoding formats:
0133: *
0134: * upper two bits units field length approximate range
0135: * ---------------------------------------------------------------
0136: * 00 30 minutes 1 byte +/- 16 hours
0137: * 01 minutes 4 bytes +/- 1020 years
0138: * 10 seconds 5 bytes +/- 4355 years
0139: * 11 millis 9 bytes +/- 292,000,000 years
0140: *
0141: * Remaining bits in field form signed offset from 1970-01-01T00:00:00Z.
0142: */
0143: static void writeMillis(DataOutput out, long millis)
0144: throws IOException {
0145: if (millis % (30 * 60000L) == 0) {
0146: // Try to write in 30 minute units.
0147: long units = millis / (30 * 60000L);
0148: if (((units << (64 - 6)) >> (64 - 6)) == units) {
0149: // Form 00 (6 bits effective precision)
0150: out.writeByte((int) (units & 0x3f));
0151: return;
0152: }
0153: }
0154:
0155: if (millis % 60000L == 0) {
0156: // Try to write minutes.
0157: long minutes = millis / 60000L;
0158: if (((minutes << (64 - 30)) >> (64 - 30)) == minutes) {
0159: // Form 01 (30 bits effective precision)
0160: out.writeInt(0x40000000 | (int) (minutes & 0x3fffffff));
0161: return;
0162: }
0163: }
0164:
0165: if (millis % 1000L == 0) {
0166: // Try to write seconds.
0167: long seconds = millis / 1000L;
0168: if (((seconds << (64 - 38)) >> (64 - 38)) == seconds) {
0169: // Form 10 (38 bits effective precision)
0170: out.writeByte(0x80 | (int) ((seconds >> 32) & 0x3f));
0171: out.writeInt((int) (seconds & 0xffffffff));
0172: return;
0173: }
0174: }
0175:
0176: // Write milliseconds either because the additional precision is
0177: // required or the minutes didn't fit in the field.
0178:
0179: // Form 11 (64 bits effective precision, but write as if 70 bits)
0180: out.writeByte(millis < 0 ? 0xff : 0xc0);
0181: out.writeLong(millis);
0182: }
0183:
0184: /**
0185: * Reads encoding generated by writeMillis.
0186: */
0187: static long readMillis(DataInput in) throws IOException {
0188: int v = in.readUnsignedByte();
0189: switch (v >> 6) {
0190: case 0:
0191: default:
0192: // Form 00 (6 bits effective precision)
0193: v = (v << (32 - 6)) >> (32 - 6);
0194: return v * (30 * 60000L);
0195:
0196: case 1:
0197: // Form 01 (30 bits effective precision)
0198: v = (v << (32 - 6)) >> (32 - 30);
0199: v |= (in.readUnsignedByte()) << 16;
0200: v |= (in.readUnsignedByte()) << 8;
0201: v |= (in.readUnsignedByte());
0202: return v * 60000L;
0203:
0204: case 2:
0205: // Form 10 (38 bits effective precision)
0206: long w = (((long) v) << (64 - 6)) >> (64 - 38);
0207: w |= (in.readUnsignedByte()) << 24;
0208: w |= (in.readUnsignedByte()) << 16;
0209: w |= (in.readUnsignedByte()) << 8;
0210: w |= (in.readUnsignedByte());
0211: return w * 1000L;
0212:
0213: case 3:
0214: // Form 11 (64 bits effective precision)
0215: return in.readLong();
0216: }
0217: }
0218:
0219: private static DateTimeZone buildFixedZone(String id,
0220: String nameKey, int wallOffset, int standardOffset) {
0221: if ("UTC".equals(id) && id.equals(nameKey) && wallOffset == 0
0222: && standardOffset == 0) {
0223: return DateTimeZone.UTC;
0224: }
0225: return new FixedDateTimeZone(id, nameKey, wallOffset,
0226: standardOffset);
0227: }
0228:
0229: // List of RuleSets.
0230: private final ArrayList iRuleSets;
0231:
0232: public DateTimeZoneBuilder() {
0233: iRuleSets = new ArrayList(10);
0234: }
0235:
0236: /**
0237: * Adds a cutover for added rules. The standard offset at the cutover
0238: * defaults to 0. Call setStandardOffset afterwards to change it.
0239: *
0240: * @param year year of cutover
0241: * @param mode 'u' - cutover is measured against UTC, 'w' - against wall
0242: * offset, 's' - against standard offset.
0243: * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth).
0244: * For example, if -1, set to last day of month
0245: * @param dayOfWeek if 0, ignore
0246: * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to
0247: * dayOfWeek when true, retreat when false.
0248: * @param millisOfDay additional precision for specifying time of day of
0249: * cutover
0250: */
0251: public DateTimeZoneBuilder addCutover(int year, char mode,
0252: int monthOfYear, int dayOfMonth, int dayOfWeek,
0253: boolean advanceDayOfWeek, int millisOfDay) {
0254: OfYear ofYear = new OfYear(mode, monthOfYear, dayOfMonth,
0255: dayOfWeek, advanceDayOfWeek, millisOfDay);
0256: if (iRuleSets.size() > 0) {
0257: RuleSet lastRuleSet = (RuleSet) iRuleSets.get(iRuleSets
0258: .size() - 1);
0259: lastRuleSet.setUpperLimit(year, ofYear);
0260: }
0261: iRuleSets.add(new RuleSet());
0262: return this ;
0263: }
0264:
0265: /**
0266: * Sets the standard offset to use for newly added rules until the next
0267: * cutover is added.
0268: */
0269: public DateTimeZoneBuilder setStandardOffset(int standardOffset) {
0270: getLastRuleSet().setStandardOffset(standardOffset);
0271: return this ;
0272: }
0273:
0274: /**
0275: * Set a fixed savings rule at the cutover.
0276: */
0277: public DateTimeZoneBuilder setFixedSavings(String nameKey,
0278: int saveMillis) {
0279: getLastRuleSet().setFixedSavings(nameKey, saveMillis);
0280: return this ;
0281: }
0282:
0283: /**
0284: * Add a recurring daylight saving time rule.
0285: *
0286: * @param nameKey name key of new rule
0287: * @param saveMillis milliseconds to add to standard offset
0288: * @param fromYear First year that rule is in effect. MIN_VALUE indicates
0289: * beginning of time.
0290: * @param toYear Last year (inclusive) that rule is in effect. MAX_VALUE
0291: * indicates end of time.
0292: * @param mode 'u' - transitions are calculated against UTC, 'w' -
0293: * transitions are calculated against wall offset, 's' - transitions are
0294: * calculated against standard offset.
0295: * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth).
0296: * For example, if -1, set to last day of month
0297: * @param dayOfWeek if 0, ignore
0298: * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to
0299: * dayOfWeek when true, retreat when false.
0300: * @param millisOfDay additional precision for specifying time of day of
0301: * transitions
0302: */
0303: public DateTimeZoneBuilder addRecurringSavings(String nameKey,
0304: int saveMillis, int fromYear, int toYear, char mode,
0305: int monthOfYear, int dayOfMonth, int dayOfWeek,
0306: boolean advanceDayOfWeek, int millisOfDay) {
0307: if (fromYear <= toYear) {
0308: OfYear ofYear = new OfYear(mode, monthOfYear, dayOfMonth,
0309: dayOfWeek, advanceDayOfWeek, millisOfDay);
0310: Recurrence recurrence = new Recurrence(ofYear, nameKey,
0311: saveMillis);
0312: Rule rule = new Rule(recurrence, fromYear, toYear);
0313: getLastRuleSet().addRule(rule);
0314: }
0315: return this ;
0316: }
0317:
0318: private RuleSet getLastRuleSet() {
0319: if (iRuleSets.size() == 0) {
0320: addCutover(Integer.MIN_VALUE, 'w', 1, 1, 0, false, 0);
0321: }
0322: return (RuleSet) iRuleSets.get(iRuleSets.size() - 1);
0323: }
0324:
0325: /**
0326: * Processes all the rules and builds a DateTimeZone.
0327: *
0328: * @param id time zone id to assign
0329: * @param outputID true if the zone id should be output
0330: */
0331: public DateTimeZone toDateTimeZone(String id, boolean outputID) {
0332: if (id == null) {
0333: throw new IllegalArgumentException();
0334: }
0335:
0336: // Discover where all the transitions occur and store the results in
0337: // these lists.
0338: ArrayList transitions = new ArrayList();
0339:
0340: // Tail zone picks up remaining transitions in the form of an endless
0341: // DST cycle.
0342: DSTZone tailZone = null;
0343:
0344: long millis = Long.MIN_VALUE;
0345: int saveMillis = 0;
0346:
0347: int ruleSetCount = iRuleSets.size();
0348: for (int i = 0; i < ruleSetCount; i++) {
0349: RuleSet rs = (RuleSet) iRuleSets.get(i);
0350: Transition next = rs.firstTransition(millis);
0351: if (next == null) {
0352: continue;
0353: }
0354: addTransition(transitions, next);
0355: millis = next.getMillis();
0356: saveMillis = next.getSaveMillis();
0357:
0358: // Copy it since we're going to destroy it.
0359: rs = new RuleSet(rs);
0360:
0361: while ((next = rs.nextTransition(millis, saveMillis)) != null) {
0362: if (addTransition(transitions, next)) {
0363: if (tailZone != null) {
0364: // Got the extra transition before DSTZone.
0365: break;
0366: }
0367: }
0368: millis = next.getMillis();
0369: saveMillis = next.getSaveMillis();
0370: if (tailZone == null && i == ruleSetCount - 1) {
0371: tailZone = rs.buildTailZone(id);
0372: // If tailZone is not null, don't break out of main loop until
0373: // at least one more transition is calculated. This ensures a
0374: // correct 'seam' to the DSTZone.
0375: }
0376: }
0377:
0378: millis = rs.getUpperLimit(saveMillis);
0379: }
0380:
0381: // Check if a simpler zone implementation can be returned.
0382: if (transitions.size() == 0) {
0383: if (tailZone != null) {
0384: // This shouldn't happen, but handle just in case.
0385: return tailZone;
0386: }
0387: return buildFixedZone(id, "UTC", 0, 0);
0388: }
0389: if (transitions.size() == 1 && tailZone == null) {
0390: Transition tr = (Transition) transitions.get(0);
0391: return buildFixedZone(id, tr.getNameKey(), tr
0392: .getWallOffset(), tr.getStandardOffset());
0393: }
0394:
0395: PrecalculatedZone zone = PrecalculatedZone.create(id, outputID,
0396: transitions, tailZone);
0397: if (zone.isCachable()) {
0398: return CachedDateTimeZone.forZone(zone);
0399: }
0400: return zone;
0401: }
0402:
0403: private boolean addTransition(ArrayList transitions, Transition tr) {
0404: int size = transitions.size();
0405: if (size == 0) {
0406: transitions.add(tr);
0407: return true;
0408: }
0409:
0410: Transition last = (Transition) transitions.get(size - 1);
0411: if (!tr.isTransitionFrom(last)) {
0412: return false;
0413: }
0414:
0415: // If local time of new transition is same as last local time, just
0416: // replace last transition with new one.
0417: int offsetForLast = 0;
0418: if (size >= 2) {
0419: offsetForLast = ((Transition) transitions.get(size - 2))
0420: .getWallOffset();
0421: }
0422: int offsetForNew = last.getWallOffset();
0423:
0424: long lastLocal = last.getMillis() + offsetForLast;
0425: long newLocal = tr.getMillis() + offsetForNew;
0426:
0427: if (newLocal != lastLocal) {
0428: transitions.add(tr);
0429: return true;
0430: }
0431:
0432: transitions.remove(size - 1);
0433: return addTransition(transitions, tr);
0434: }
0435:
0436: /**
0437: * Encodes a built DateTimeZone to the given stream. Call readFrom to
0438: * decode the data into a DateTimeZone object.
0439: *
0440: * @param out output stream to receive encoded DateTimeZone.
0441: * @since 1.5 (parameter added)
0442: */
0443: public void writeTo(String zoneID, OutputStream out)
0444: throws IOException {
0445: if (out instanceof DataOutput) {
0446: writeTo(zoneID, (DataOutput) out);
0447: } else {
0448: writeTo(zoneID, (DataOutput) new DataOutputStream(out));
0449: }
0450: }
0451:
0452: /**
0453: * Encodes a built DateTimeZone to the given stream. Call readFrom to
0454: * decode the data into a DateTimeZone object.
0455: *
0456: * @param out output stream to receive encoded DateTimeZone.
0457: * @since 1.5 (parameter added)
0458: */
0459: public void writeTo(String zoneID, DataOutput out)
0460: throws IOException {
0461: // pass false so zone id is not written out
0462: DateTimeZone zone = toDateTimeZone(zoneID, false);
0463:
0464: if (zone instanceof FixedDateTimeZone) {
0465: out.writeByte('F'); // 'F' for fixed
0466: out.writeUTF(zone.getNameKey(0));
0467: writeMillis(out, zone.getOffset(0));
0468: writeMillis(out, zone.getStandardOffset(0));
0469: } else {
0470: if (zone instanceof CachedDateTimeZone) {
0471: out.writeByte('C'); // 'C' for cached, precalculated
0472: zone = ((CachedDateTimeZone) zone).getUncachedZone();
0473: } else {
0474: out.writeByte('P'); // 'P' for precalculated, uncached
0475: }
0476: ((PrecalculatedZone) zone).writeTo(out);
0477: }
0478: }
0479:
0480: /**
0481: * Supports setting fields of year and moving between transitions.
0482: */
0483: private static final class OfYear {
0484: static OfYear readFrom(DataInput in) throws IOException {
0485: return new OfYear((char) in.readUnsignedByte(), (int) in
0486: .readUnsignedByte(), (int) in.readByte(), (int) in
0487: .readUnsignedByte(), in.readBoolean(),
0488: (int) readMillis(in));
0489: }
0490:
0491: // Is 'u', 'w', or 's'.
0492: final char iMode;
0493:
0494: final int iMonthOfYear;
0495: final int iDayOfMonth;
0496: final int iDayOfWeek;
0497: final boolean iAdvance;
0498: final int iMillisOfDay;
0499:
0500: OfYear(char mode, int monthOfYear, int dayOfMonth,
0501: int dayOfWeek, boolean advanceDayOfWeek, int millisOfDay) {
0502: if (mode != 'u' && mode != 'w' && mode != 's') {
0503: throw new IllegalArgumentException("Unknown mode: "
0504: + mode);
0505: }
0506:
0507: iMode = mode;
0508: iMonthOfYear = monthOfYear;
0509: iDayOfMonth = dayOfMonth;
0510: iDayOfWeek = dayOfWeek;
0511: iAdvance = advanceDayOfWeek;
0512: iMillisOfDay = millisOfDay;
0513: }
0514:
0515: /**
0516: * @param standardOffset standard offset just before instant
0517: */
0518: public long setInstant(int year, int standardOffset,
0519: int saveMillis) {
0520: int offset;
0521: if (iMode == 'w') {
0522: offset = standardOffset + saveMillis;
0523: } else if (iMode == 's') {
0524: offset = standardOffset;
0525: } else {
0526: offset = 0;
0527: }
0528:
0529: Chronology chrono = ISOChronology.getInstanceUTC();
0530: long millis = chrono.year().set(0, year);
0531: millis = chrono.monthOfYear().set(millis, iMonthOfYear);
0532: millis = chrono.millisOfDay().set(millis, iMillisOfDay);
0533: millis = setDayOfMonth(chrono, millis);
0534:
0535: if (iDayOfWeek != 0) {
0536: millis = setDayOfWeek(chrono, millis);
0537: }
0538:
0539: // Convert from local time to UTC.
0540: return millis - offset;
0541: }
0542:
0543: /**
0544: * @param standardOffset standard offset just before next recurrence
0545: */
0546: public long next(long instant, int standardOffset,
0547: int saveMillis) {
0548: int offset;
0549: if (iMode == 'w') {
0550: offset = standardOffset + saveMillis;
0551: } else if (iMode == 's') {
0552: offset = standardOffset;
0553: } else {
0554: offset = 0;
0555: }
0556:
0557: // Convert from UTC to local time.
0558: instant += offset;
0559:
0560: Chronology chrono = ISOChronology.getInstanceUTC();
0561: long next = chrono.monthOfYear().set(instant, iMonthOfYear);
0562: // Be lenient with millisOfDay.
0563: next = chrono.millisOfDay().set(next, 0);
0564: next = chrono.millisOfDay().add(next, iMillisOfDay);
0565: next = setDayOfMonthNext(chrono, next);
0566:
0567: if (iDayOfWeek == 0) {
0568: if (next <= instant) {
0569: next = chrono.year().add(next, 1);
0570: next = setDayOfMonthNext(chrono, next);
0571: }
0572: } else {
0573: next = setDayOfWeek(chrono, next);
0574: if (next <= instant) {
0575: next = chrono.year().add(next, 1);
0576: next = chrono.monthOfYear().set(next, iMonthOfYear);
0577: next = setDayOfMonthNext(chrono, next);
0578: next = setDayOfWeek(chrono, next);
0579: }
0580: }
0581:
0582: // Convert from local time to UTC.
0583: return next - offset;
0584: }
0585:
0586: /**
0587: * @param standardOffset standard offset just before previous recurrence
0588: */
0589: public long previous(long instant, int standardOffset,
0590: int saveMillis) {
0591: int offset;
0592: if (iMode == 'w') {
0593: offset = standardOffset + saveMillis;
0594: } else if (iMode == 's') {
0595: offset = standardOffset;
0596: } else {
0597: offset = 0;
0598: }
0599:
0600: // Convert from UTC to local time.
0601: instant += offset;
0602:
0603: Chronology chrono = ISOChronology.getInstanceUTC();
0604: long prev = chrono.monthOfYear().set(instant, iMonthOfYear);
0605: // Be lenient with millisOfDay.
0606: prev = chrono.millisOfDay().set(prev, 0);
0607: prev = chrono.millisOfDay().add(prev, iMillisOfDay);
0608: prev = setDayOfMonthPrevious(chrono, prev);
0609:
0610: if (iDayOfWeek == 0) {
0611: if (prev >= instant) {
0612: prev = chrono.year().add(prev, -1);
0613: prev = setDayOfMonthPrevious(chrono, prev);
0614: }
0615: } else {
0616: prev = setDayOfWeek(chrono, prev);
0617: if (prev >= instant) {
0618: prev = chrono.year().add(prev, -1);
0619: prev = chrono.monthOfYear().set(prev, iMonthOfYear);
0620: prev = setDayOfMonthPrevious(chrono, prev);
0621: prev = setDayOfWeek(chrono, prev);
0622: }
0623: }
0624:
0625: // Convert from local time to UTC.
0626: return prev - offset;
0627: }
0628:
0629: public boolean equals(Object obj) {
0630: if (this == obj) {
0631: return true;
0632: }
0633: if (obj instanceof OfYear) {
0634: OfYear other = (OfYear) obj;
0635: return iMode == other.iMode
0636: && iMonthOfYear == other.iMonthOfYear
0637: && iDayOfMonth == other.iDayOfMonth
0638: && iDayOfWeek == other.iDayOfWeek
0639: && iAdvance == other.iAdvance
0640: && iMillisOfDay == other.iMillisOfDay;
0641: }
0642: return false;
0643: }
0644:
0645: /*
0646: public String toString() {
0647: return
0648: "[OfYear]\n" +
0649: "Mode: " + iMode + '\n' +
0650: "MonthOfYear: " + iMonthOfYear + '\n' +
0651: "DayOfMonth: " + iDayOfMonth + '\n' +
0652: "DayOfWeek: " + iDayOfWeek + '\n' +
0653: "AdvanceDayOfWeek: " + iAdvance + '\n' +
0654: "MillisOfDay: " + iMillisOfDay + '\n';
0655: }
0656: */
0657:
0658: public void writeTo(DataOutput out) throws IOException {
0659: out.writeByte(iMode);
0660: out.writeByte(iMonthOfYear);
0661: out.writeByte(iDayOfMonth);
0662: out.writeByte(iDayOfWeek);
0663: out.writeBoolean(iAdvance);
0664: writeMillis(out, iMillisOfDay);
0665: }
0666:
0667: /**
0668: * If month-day is 02-29 and year isn't leap, advances to next leap year.
0669: */
0670: private long setDayOfMonthNext(Chronology chrono, long next) {
0671: try {
0672: next = setDayOfMonth(chrono, next);
0673: } catch (IllegalArgumentException e) {
0674: if (iMonthOfYear == 2 && iDayOfMonth == 29) {
0675: while (chrono.year().isLeap(next) == false) {
0676: next = chrono.year().add(next, 1);
0677: }
0678: next = setDayOfMonth(chrono, next);
0679: } else {
0680: throw e;
0681: }
0682: }
0683: return next;
0684: }
0685:
0686: /**
0687: * If month-day is 02-29 and year isn't leap, retreats to previous leap year.
0688: */
0689: private long setDayOfMonthPrevious(Chronology chrono, long prev) {
0690: try {
0691: prev = setDayOfMonth(chrono, prev);
0692: } catch (IllegalArgumentException e) {
0693: if (iMonthOfYear == 2 && iDayOfMonth == 29) {
0694: while (chrono.year().isLeap(prev) == false) {
0695: prev = chrono.year().add(prev, -1);
0696: }
0697: prev = setDayOfMonth(chrono, prev);
0698: } else {
0699: throw e;
0700: }
0701: }
0702: return prev;
0703: }
0704:
0705: private long setDayOfMonth(Chronology chrono, long instant) {
0706: if (iDayOfMonth >= 0) {
0707: instant = chrono.dayOfMonth().set(instant, iDayOfMonth);
0708: } else {
0709: instant = chrono.dayOfMonth().set(instant, 1);
0710: instant = chrono.monthOfYear().add(instant, 1);
0711: instant = chrono.dayOfMonth().add(instant, iDayOfMonth);
0712: }
0713: return instant;
0714: }
0715:
0716: private long setDayOfWeek(Chronology chrono, long instant) {
0717: int dayOfWeek = chrono.dayOfWeek().get(instant);
0718: int daysToAdd = iDayOfWeek - dayOfWeek;
0719: if (daysToAdd != 0) {
0720: if (iAdvance) {
0721: if (daysToAdd < 0) {
0722: daysToAdd += 7;
0723: }
0724: } else {
0725: if (daysToAdd > 0) {
0726: daysToAdd -= 7;
0727: }
0728: }
0729: instant = chrono.dayOfWeek().add(instant, daysToAdd);
0730: }
0731: return instant;
0732: }
0733: }
0734:
0735: /**
0736: * Extends OfYear with a nameKey and savings.
0737: */
0738: private static final class Recurrence {
0739: static Recurrence readFrom(DataInput in) throws IOException {
0740: return new Recurrence(OfYear.readFrom(in), in.readUTF(),
0741: (int) readMillis(in));
0742: }
0743:
0744: final OfYear iOfYear;
0745: final String iNameKey;
0746: final int iSaveMillis;
0747:
0748: Recurrence(OfYear ofYear, String nameKey, int saveMillis) {
0749: iOfYear = ofYear;
0750: iNameKey = nameKey;
0751: iSaveMillis = saveMillis;
0752: }
0753:
0754: public OfYear getOfYear() {
0755: return iOfYear;
0756: }
0757:
0758: /**
0759: * @param standardOffset standard offset just before next recurrence
0760: */
0761: public long next(long instant, int standardOffset,
0762: int saveMillis) {
0763: return iOfYear.next(instant, standardOffset, saveMillis);
0764: }
0765:
0766: /**
0767: * @param standardOffset standard offset just before previous recurrence
0768: */
0769: public long previous(long instant, int standardOffset,
0770: int saveMillis) {
0771: return iOfYear
0772: .previous(instant, standardOffset, saveMillis);
0773: }
0774:
0775: public String getNameKey() {
0776: return iNameKey;
0777: }
0778:
0779: public int getSaveMillis() {
0780: return iSaveMillis;
0781: }
0782:
0783: public boolean equals(Object obj) {
0784: if (this == obj) {
0785: return true;
0786: }
0787: if (obj instanceof Recurrence) {
0788: Recurrence other = (Recurrence) obj;
0789: return iSaveMillis == other.iSaveMillis
0790: && iNameKey.equals(other.iNameKey)
0791: && iOfYear.equals(other.iOfYear);
0792: }
0793: return false;
0794: }
0795:
0796: public void writeTo(DataOutput out) throws IOException {
0797: iOfYear.writeTo(out);
0798: out.writeUTF(iNameKey);
0799: writeMillis(out, iSaveMillis);
0800: }
0801:
0802: Recurrence rename(String nameKey) {
0803: return new Recurrence(iOfYear, nameKey, iSaveMillis);
0804: }
0805:
0806: Recurrence renameAppend(String appendNameKey) {
0807: return rename((iNameKey + appendNameKey).intern());
0808: }
0809: }
0810:
0811: /**
0812: * Extends Recurrence with inclusive year limits.
0813: */
0814: private static final class Rule {
0815: final Recurrence iRecurrence;
0816: final int iFromYear; // inclusive
0817: final int iToYear; // inclusive
0818:
0819: Rule(Recurrence recurrence, int fromYear, int toYear) {
0820: iRecurrence = recurrence;
0821: iFromYear = fromYear;
0822: iToYear = toYear;
0823: }
0824:
0825: public int getFromYear() {
0826: return iFromYear;
0827: }
0828:
0829: public int getToYear() {
0830: return iToYear;
0831: }
0832:
0833: public OfYear getOfYear() {
0834: return iRecurrence.getOfYear();
0835: }
0836:
0837: public String getNameKey() {
0838: return iRecurrence.getNameKey();
0839: }
0840:
0841: public int getSaveMillis() {
0842: return iRecurrence.getSaveMillis();
0843: }
0844:
0845: public long next(final long instant, int standardOffset,
0846: int saveMillis) {
0847: Chronology chrono = ISOChronology.getInstanceUTC();
0848:
0849: final int wallOffset = standardOffset + saveMillis;
0850: long testInstant = instant;
0851:
0852: int year;
0853: if (instant == Long.MIN_VALUE) {
0854: year = Integer.MIN_VALUE;
0855: } else {
0856: year = chrono.year().get(instant + wallOffset);
0857: }
0858:
0859: if (year < iFromYear) {
0860: // First advance instant to start of from year.
0861: testInstant = chrono.year().set(0, iFromYear)
0862: - wallOffset;
0863: // Back off one millisecond to account for next recurrence
0864: // being exactly at the beginning of the year.
0865: testInstant -= 1;
0866: }
0867:
0868: long next = iRecurrence.next(testInstant, standardOffset,
0869: saveMillis);
0870:
0871: if (next > instant) {
0872: year = chrono.year().get(next + wallOffset);
0873: if (year > iToYear) {
0874: // Out of range, return original value.
0875: next = instant;
0876: }
0877: }
0878:
0879: return next;
0880: }
0881: }
0882:
0883: private static final class Transition {
0884: private final long iMillis;
0885: private final String iNameKey;
0886: private final int iWallOffset;
0887: private final int iStandardOffset;
0888:
0889: Transition(long millis, Transition tr) {
0890: iMillis = millis;
0891: iNameKey = tr.iNameKey;
0892: iWallOffset = tr.iWallOffset;
0893: iStandardOffset = tr.iStandardOffset;
0894: }
0895:
0896: Transition(long millis, Rule rule, int standardOffset) {
0897: iMillis = millis;
0898: iNameKey = rule.getNameKey();
0899: iWallOffset = standardOffset + rule.getSaveMillis();
0900: iStandardOffset = standardOffset;
0901: }
0902:
0903: Transition(long millis, String nameKey, int wallOffset,
0904: int standardOffset) {
0905: iMillis = millis;
0906: iNameKey = nameKey;
0907: iWallOffset = wallOffset;
0908: iStandardOffset = standardOffset;
0909: }
0910:
0911: public long getMillis() {
0912: return iMillis;
0913: }
0914:
0915: public String getNameKey() {
0916: return iNameKey;
0917: }
0918:
0919: public int getWallOffset() {
0920: return iWallOffset;
0921: }
0922:
0923: public int getStandardOffset() {
0924: return iStandardOffset;
0925: }
0926:
0927: public int getSaveMillis() {
0928: return iWallOffset - iStandardOffset;
0929: }
0930:
0931: /**
0932: * There must be a change in the millis, wall offsets or name keys.
0933: */
0934: public boolean isTransitionFrom(Transition other) {
0935: if (other == null) {
0936: return true;
0937: }
0938: return iMillis > other.iMillis
0939: && (iWallOffset != other.iWallOffset ||
0940: //iStandardOffset != other.iStandardOffset ||
0941: !(iNameKey.equals(other.iNameKey)));
0942: }
0943: }
0944:
0945: private static final class RuleSet {
0946: private static final int YEAR_LIMIT;
0947:
0948: static {
0949: // Don't pre-calculate more than 100 years into the future. Almost
0950: // all zones will stop pre-calculating far sooner anyhow. Either a
0951: // simple DST cycle is detected or the last rule is a fixed
0952: // offset. If a zone has a fixed offset set more than 100 years
0953: // into the future, then it won't be observed.
0954: long now = DateTimeUtils.currentTimeMillis();
0955: YEAR_LIMIT = ISOChronology.getInstanceUTC().year().get(now) + 100;
0956: }
0957:
0958: private int iStandardOffset;
0959: private ArrayList iRules;
0960:
0961: // Optional.
0962: private String iInitialNameKey;
0963: private int iInitialSaveMillis;
0964:
0965: // Upper limit is exclusive.
0966: private int iUpperYear;
0967: private OfYear iUpperOfYear;
0968:
0969: RuleSet() {
0970: iRules = new ArrayList(10);
0971: iUpperYear = Integer.MAX_VALUE;
0972: }
0973:
0974: /**
0975: * Copy constructor.
0976: */
0977: RuleSet(RuleSet rs) {
0978: iStandardOffset = rs.iStandardOffset;
0979: iRules = new ArrayList(rs.iRules);
0980: iInitialNameKey = rs.iInitialNameKey;
0981: iInitialSaveMillis = rs.iInitialSaveMillis;
0982: iUpperYear = rs.iUpperYear;
0983: iUpperOfYear = rs.iUpperOfYear;
0984: }
0985:
0986: public int getStandardOffset() {
0987: return iStandardOffset;
0988: }
0989:
0990: public void setStandardOffset(int standardOffset) {
0991: iStandardOffset = standardOffset;
0992: }
0993:
0994: public void setFixedSavings(String nameKey, int saveMillis) {
0995: iInitialNameKey = nameKey;
0996: iInitialSaveMillis = saveMillis;
0997: }
0998:
0999: public void addRule(Rule rule) {
1000: if (!iRules.contains(rule)) {
1001: iRules.add(rule);
1002: }
1003: }
1004:
1005: public void setUpperLimit(int year, OfYear ofYear) {
1006: iUpperYear = year;
1007: iUpperOfYear = ofYear;
1008: }
1009:
1010: /**
1011: * Returns a transition at firstMillis with the first name key and
1012: * offsets for this rule set. This method may return null.
1013: *
1014: * @param firstMillis millis of first transition
1015: */
1016: public Transition firstTransition(final long firstMillis) {
1017: if (iInitialNameKey != null) {
1018: // Initial zone info explicitly set, so don't search the rules.
1019: return new Transition(firstMillis, iInitialNameKey,
1020: iStandardOffset + iInitialSaveMillis,
1021: iStandardOffset);
1022: }
1023:
1024: // Make a copy before we destroy the rules.
1025: ArrayList copy = new ArrayList(iRules);
1026:
1027: // Iterate through all the transitions until firstMillis is
1028: // reached. Use the name key and savings for whatever rule reaches
1029: // the limit.
1030:
1031: long millis = Long.MIN_VALUE;
1032: int saveMillis = 0;
1033: Transition first = null;
1034:
1035: Transition next;
1036: while ((next = nextTransition(millis, saveMillis)) != null) {
1037: millis = next.getMillis();
1038:
1039: if (millis == firstMillis) {
1040: first = new Transition(firstMillis, next);
1041: break;
1042: }
1043:
1044: if (millis > firstMillis) {
1045: if (first == null) {
1046: // Find first rule without savings. This way a more
1047: // accurate nameKey is found even though no rule
1048: // extends to the RuleSet's lower limit.
1049: Iterator it = copy.iterator();
1050: while (it.hasNext()) {
1051: Rule rule = (Rule) it.next();
1052: if (rule.getSaveMillis() == 0) {
1053: first = new Transition(firstMillis,
1054: rule, iStandardOffset);
1055: break;
1056: }
1057: }
1058: }
1059: if (first == null) {
1060: // Found no rule without savings. Create a transition
1061: // with no savings anyhow, and use the best available
1062: // name key.
1063: first = new Transition(firstMillis, next
1064: .getNameKey(), iStandardOffset,
1065: iStandardOffset);
1066: }
1067: break;
1068: }
1069:
1070: // Set first to the best transition found so far, but next
1071: // iteration may find something closer to lower limit.
1072: first = new Transition(firstMillis, next);
1073:
1074: saveMillis = next.getSaveMillis();
1075: }
1076:
1077: iRules = copy;
1078: return first;
1079: }
1080:
1081: /**
1082: * Returns null if RuleSet is exhausted or upper limit reached. Calling
1083: * this method will throw away rules as they each become
1084: * exhausted. Copy the RuleSet before using it to compute transitions.
1085: *
1086: * Returned transition may be a duplicate from previous
1087: * transition. Caller must call isTransitionFrom to filter out
1088: * duplicates.
1089: *
1090: * @param saveMillis savings before next transition
1091: */
1092: public Transition nextTransition(final long instant,
1093: final int saveMillis) {
1094: Chronology chrono = ISOChronology.getInstanceUTC();
1095:
1096: // Find next matching rule.
1097: Rule nextRule = null;
1098: long nextMillis = Long.MAX_VALUE;
1099:
1100: Iterator it = iRules.iterator();
1101: while (it.hasNext()) {
1102: Rule rule = (Rule) it.next();
1103: long next = rule.next(instant, iStandardOffset,
1104: saveMillis);
1105: if (next <= instant) {
1106: it.remove();
1107: continue;
1108: }
1109: // Even if next is same as previous next, choose the rule
1110: // in order for more recently added rules to override.
1111: if (next <= nextMillis) {
1112: // Found a better match.
1113: nextRule = rule;
1114: nextMillis = next;
1115: }
1116: }
1117:
1118: if (nextRule == null) {
1119: return null;
1120: }
1121:
1122: // Stop precalculating if year reaches some arbitrary limit.
1123: if (chrono.year().get(nextMillis) >= YEAR_LIMIT) {
1124: return null;
1125: }
1126:
1127: // Check if upper limit reached or passed.
1128: if (iUpperYear < Integer.MAX_VALUE) {
1129: long upperMillis = iUpperOfYear.setInstant(iUpperYear,
1130: iStandardOffset, saveMillis);
1131: if (nextMillis >= upperMillis) {
1132: // At or after upper limit.
1133: return null;
1134: }
1135: }
1136:
1137: return new Transition(nextMillis, nextRule, iStandardOffset);
1138: }
1139:
1140: /**
1141: * @param saveMillis savings before upper limit
1142: */
1143: public long getUpperLimit(int saveMillis) {
1144: if (iUpperYear == Integer.MAX_VALUE) {
1145: return Long.MAX_VALUE;
1146: }
1147: return iUpperOfYear.setInstant(iUpperYear, iStandardOffset,
1148: saveMillis);
1149: }
1150:
1151: /**
1152: * Returns null if none can be built.
1153: */
1154: public DSTZone buildTailZone(String id) {
1155: if (iRules.size() == 2) {
1156: Rule startRule = (Rule) iRules.get(0);
1157: Rule endRule = (Rule) iRules.get(1);
1158: if (startRule.getToYear() == Integer.MAX_VALUE
1159: && endRule.getToYear() == Integer.MAX_VALUE) {
1160:
1161: // With exactly two infinitely recurring rules left, a
1162: // simple DSTZone can be formed.
1163:
1164: // The order of rules can come in any order, and it doesn't
1165: // really matter which rule was chosen the 'start' and
1166: // which is chosen the 'end'. DSTZone works properly either
1167: // way.
1168: return new DSTZone(id, iStandardOffset,
1169: startRule.iRecurrence, endRule.iRecurrence);
1170: }
1171: }
1172: return null;
1173: }
1174: }
1175:
1176: private static final class DSTZone extends DateTimeZone {
1177: private static final long serialVersionUID = 6941492635554961361L;
1178:
1179: static DSTZone readFrom(DataInput in, String id)
1180: throws IOException {
1181: return new DSTZone(id, (int) readMillis(in), Recurrence
1182: .readFrom(in), Recurrence.readFrom(in));
1183: }
1184:
1185: final int iStandardOffset;
1186: final Recurrence iStartRecurrence;
1187: final Recurrence iEndRecurrence;
1188:
1189: DSTZone(String id, int standardOffset,
1190: Recurrence startRecurrence, Recurrence endRecurrence) {
1191: super (id);
1192: iStandardOffset = standardOffset;
1193: iStartRecurrence = startRecurrence;
1194: iEndRecurrence = endRecurrence;
1195: }
1196:
1197: public String getNameKey(long instant) {
1198: return findMatchingRecurrence(instant).getNameKey();
1199: }
1200:
1201: public int getOffset(long instant) {
1202: return iStandardOffset
1203: + findMatchingRecurrence(instant).getSaveMillis();
1204: }
1205:
1206: public int getStandardOffset(long instant) {
1207: return iStandardOffset;
1208: }
1209:
1210: public boolean isFixed() {
1211: return false;
1212: }
1213:
1214: public long nextTransition(long instant) {
1215: int standardOffset = iStandardOffset;
1216: Recurrence startRecurrence = iStartRecurrence;
1217: Recurrence endRecurrence = iEndRecurrence;
1218:
1219: long start, end;
1220:
1221: try {
1222: start = startRecurrence.next(instant, standardOffset,
1223: endRecurrence.getSaveMillis());
1224: if (instant > 0 && start < 0) {
1225: // Overflowed.
1226: start = instant;
1227: }
1228: } catch (IllegalArgumentException e) {
1229: // Overflowed.
1230: start = instant;
1231: } catch (ArithmeticException e) {
1232: // Overflowed.
1233: start = instant;
1234: }
1235:
1236: try {
1237: end = endRecurrence.next(instant, standardOffset,
1238: startRecurrence.getSaveMillis());
1239: if (instant > 0 && end < 0) {
1240: // Overflowed.
1241: end = instant;
1242: }
1243: } catch (IllegalArgumentException e) {
1244: // Overflowed.
1245: end = instant;
1246: } catch (ArithmeticException e) {
1247: // Overflowed.
1248: end = instant;
1249: }
1250:
1251: return (start > end) ? end : start;
1252: }
1253:
1254: public long previousTransition(long instant) {
1255: // Increment in order to handle the case where instant is exactly at
1256: // a transition.
1257: instant++;
1258:
1259: int standardOffset = iStandardOffset;
1260: Recurrence startRecurrence = iStartRecurrence;
1261: Recurrence endRecurrence = iEndRecurrence;
1262:
1263: long start, end;
1264:
1265: try {
1266: start = startRecurrence.previous(instant,
1267: standardOffset, endRecurrence.getSaveMillis());
1268: if (instant < 0 && start > 0) {
1269: // Overflowed.
1270: start = instant;
1271: }
1272: } catch (IllegalArgumentException e) {
1273: // Overflowed.
1274: start = instant;
1275: } catch (ArithmeticException e) {
1276: // Overflowed.
1277: start = instant;
1278: }
1279:
1280: try {
1281: end = endRecurrence.previous(instant, standardOffset,
1282: startRecurrence.getSaveMillis());
1283: if (instant < 0 && end > 0) {
1284: // Overflowed.
1285: end = instant;
1286: }
1287: } catch (IllegalArgumentException e) {
1288: // Overflowed.
1289: end = instant;
1290: } catch (ArithmeticException e) {
1291: // Overflowed.
1292: end = instant;
1293: }
1294:
1295: return ((start > end) ? start : end) - 1;
1296: }
1297:
1298: public boolean equals(Object obj) {
1299: if (this == obj) {
1300: return true;
1301: }
1302: if (obj instanceof DSTZone) {
1303: DSTZone other = (DSTZone) obj;
1304: return getID().equals(other.getID())
1305: && iStandardOffset == other.iStandardOffset
1306: && iStartRecurrence
1307: .equals(other.iStartRecurrence)
1308: && iEndRecurrence.equals(other.iEndRecurrence);
1309: }
1310: return false;
1311: }
1312:
1313: public void writeTo(DataOutput out) throws IOException {
1314: writeMillis(out, iStandardOffset);
1315: iStartRecurrence.writeTo(out);
1316: iEndRecurrence.writeTo(out);
1317: }
1318:
1319: private Recurrence findMatchingRecurrence(long instant) {
1320: int standardOffset = iStandardOffset;
1321: Recurrence startRecurrence = iStartRecurrence;
1322: Recurrence endRecurrence = iEndRecurrence;
1323:
1324: long start, end;
1325:
1326: try {
1327: start = startRecurrence.next(instant, standardOffset,
1328: endRecurrence.getSaveMillis());
1329: } catch (IllegalArgumentException e) {
1330: // Overflowed.
1331: start = instant;
1332: } catch (ArithmeticException e) {
1333: // Overflowed.
1334: start = instant;
1335: }
1336:
1337: try {
1338: end = endRecurrence.next(instant, standardOffset,
1339: startRecurrence.getSaveMillis());
1340: } catch (IllegalArgumentException e) {
1341: // Overflowed.
1342: end = instant;
1343: } catch (ArithmeticException e) {
1344: // Overflowed.
1345: end = instant;
1346: }
1347:
1348: return (start > end) ? startRecurrence : endRecurrence;
1349: }
1350: }
1351:
1352: private static final class PrecalculatedZone extends DateTimeZone {
1353: private static final long serialVersionUID = 7811976468055766265L;
1354:
1355: static PrecalculatedZone readFrom(DataInput in, String id)
1356: throws IOException {
1357: // Read string pool.
1358: int poolSize = in.readUnsignedShort();
1359: String[] pool = new String[poolSize];
1360: for (int i = 0; i < poolSize; i++) {
1361: pool[i] = in.readUTF();
1362: }
1363:
1364: int size = in.readInt();
1365: long[] transitions = new long[size];
1366: int[] wallOffsets = new int[size];
1367: int[] standardOffsets = new int[size];
1368: String[] nameKeys = new String[size];
1369:
1370: for (int i = 0; i < size; i++) {
1371: transitions[i] = readMillis(in);
1372: wallOffsets[i] = (int) readMillis(in);
1373: standardOffsets[i] = (int) readMillis(in);
1374: try {
1375: int index;
1376: if (poolSize < 256) {
1377: index = in.readUnsignedByte();
1378: } else {
1379: index = in.readUnsignedShort();
1380: }
1381: nameKeys[i] = pool[index];
1382: } catch (ArrayIndexOutOfBoundsException e) {
1383: throw new IOException("Invalid encoding");
1384: }
1385: }
1386:
1387: DSTZone tailZone = null;
1388: if (in.readBoolean()) {
1389: tailZone = DSTZone.readFrom(in, id);
1390: }
1391:
1392: return new PrecalculatedZone(id, transitions, wallOffsets,
1393: standardOffsets, nameKeys, tailZone);
1394: }
1395:
1396: /**
1397: * Factory to create instance from builder.
1398: *
1399: * @param id the zone id
1400: * @param outputID true if the zone id should be output
1401: * @param transitions the list of Transition objects
1402: * @param tailZone optional zone for getting info beyond precalculated tables
1403: */
1404: static PrecalculatedZone create(String id, boolean outputID,
1405: ArrayList transitions, DSTZone tailZone) {
1406: int size = transitions.size();
1407: if (size == 0) {
1408: throw new IllegalArgumentException();
1409: }
1410:
1411: long[] trans = new long[size];
1412: int[] wallOffsets = new int[size];
1413: int[] standardOffsets = new int[size];
1414: String[] nameKeys = new String[size];
1415:
1416: Transition last = null;
1417: for (int i = 0; i < size; i++) {
1418: Transition tr = (Transition) transitions.get(i);
1419:
1420: if (!tr.isTransitionFrom(last)) {
1421: throw new IllegalArgumentException(id);
1422: }
1423:
1424: trans[i] = tr.getMillis();
1425: wallOffsets[i] = tr.getWallOffset();
1426: standardOffsets[i] = tr.getStandardOffset();
1427: nameKeys[i] = tr.getNameKey();
1428:
1429: last = tr;
1430: }
1431:
1432: // Some timezones (Australia) have the same name key for
1433: // summer and winter which messes everything up. Fix it here.
1434: String[] zoneNameData = new String[5];
1435: String[][] zoneStrings = new DateFormatSymbols(
1436: Locale.ENGLISH).getZoneStrings();
1437: for (int j = 0; j < zoneStrings.length; j++) {
1438: String[] set = zoneStrings[j];
1439: if (set != null && set.length == 5 && id.equals(set[0])) {
1440: zoneNameData = set;
1441: }
1442: }
1443: for (int i = 0; i < nameKeys.length - 1; i++) {
1444: String curNameKey = nameKeys[i];
1445: String nextNameKey = nameKeys[i + 1];
1446: long curOffset = wallOffsets[i];
1447: long nextOffset = wallOffsets[i + 1];
1448: long curStdOffset = standardOffsets[i];
1449: long nextStdOffset = standardOffsets[i + 1];
1450: Period p = new Period(trans[i], trans[i + 1],
1451: PeriodType.yearMonthDay());
1452: if (curOffset != nextOffset
1453: && curStdOffset == nextStdOffset
1454: && curNameKey.equals(nextNameKey)
1455: && p.getYears() == 0 && p.getMonths() > 4
1456: && p.getMonths() < 8
1457: && curNameKey.equals(zoneNameData[2])
1458: && curNameKey.equals(zoneNameData[4])) {
1459:
1460: System.out.println("Fixing duplicate name key - "
1461: + nextNameKey);
1462: System.out.println(" - "
1463: + new DateTime(trans[i]) + " - "
1464: + new DateTime(trans[i + 1]));
1465: if (curOffset > nextOffset) {
1466: nameKeys[i] = (curNameKey + "-Summer").intern();
1467: } else if (curOffset < nextOffset) {
1468: nameKeys[i + 1] = (nextNameKey + "-Summer")
1469: .intern();
1470: i++;
1471: }
1472: }
1473: }
1474: if (tailZone != null) {
1475: if (tailZone.iStartRecurrence.getNameKey().equals(
1476: tailZone.iEndRecurrence.getNameKey())) {
1477: System.out
1478: .println("Fixing duplicate recurrent name key - "
1479: + tailZone.iStartRecurrence
1480: .getNameKey());
1481: if (tailZone.iStartRecurrence.getSaveMillis() > 0) {
1482: tailZone = new DSTZone(tailZone.getID(),
1483: tailZone.iStandardOffset,
1484: tailZone.iStartRecurrence
1485: .renameAppend("-Summer"),
1486: tailZone.iEndRecurrence);
1487: } else {
1488: tailZone = new DSTZone(tailZone.getID(),
1489: tailZone.iStandardOffset,
1490: tailZone.iStartRecurrence,
1491: tailZone.iEndRecurrence
1492: .renameAppend("-Summer"));
1493: }
1494: }
1495: }
1496:
1497: return new PrecalculatedZone((outputID ? id : ""), trans,
1498: wallOffsets, standardOffsets, nameKeys, tailZone);
1499: }
1500:
1501: // All array fields have the same length.
1502:
1503: private final long[] iTransitions;
1504:
1505: private final int[] iWallOffsets;
1506: private final int[] iStandardOffsets;
1507: private final String[] iNameKeys;
1508:
1509: private final DSTZone iTailZone;
1510:
1511: /**
1512: * Constructor used ONLY for valid input, loaded via static methods.
1513: */
1514: private PrecalculatedZone(String id, long[] transitions,
1515: int[] wallOffsets, int[] standardOffsets,
1516: String[] nameKeys, DSTZone tailZone) {
1517: super (id);
1518: iTransitions = transitions;
1519: iWallOffsets = wallOffsets;
1520: iStandardOffsets = standardOffsets;
1521: iNameKeys = nameKeys;
1522: iTailZone = tailZone;
1523: }
1524:
1525: public String getNameKey(long instant) {
1526: long[] transitions = iTransitions;
1527: int i = Arrays.binarySearch(transitions, instant);
1528: if (i >= 0) {
1529: return iNameKeys[i];
1530: }
1531: i = ~i;
1532: if (i < transitions.length) {
1533: if (i > 0) {
1534: return iNameKeys[i - 1];
1535: }
1536: return "UTC";
1537: }
1538: if (iTailZone == null) {
1539: return iNameKeys[i - 1];
1540: }
1541: return iTailZone.getNameKey(instant);
1542: }
1543:
1544: public int getOffset(long instant) {
1545: long[] transitions = iTransitions;
1546: int i = Arrays.binarySearch(transitions, instant);
1547: if (i >= 0) {
1548: return iWallOffsets[i];
1549: }
1550: i = ~i;
1551: if (i < transitions.length) {
1552: if (i > 0) {
1553: return iWallOffsets[i - 1];
1554: }
1555: return 0;
1556: }
1557: if (iTailZone == null) {
1558: return iWallOffsets[i - 1];
1559: }
1560: return iTailZone.getOffset(instant);
1561: }
1562:
1563: public int getStandardOffset(long instant) {
1564: long[] transitions = iTransitions;
1565: int i = Arrays.binarySearch(transitions, instant);
1566: if (i >= 0) {
1567: return iStandardOffsets[i];
1568: }
1569: i = ~i;
1570: if (i < transitions.length) {
1571: if (i > 0) {
1572: return iStandardOffsets[i - 1];
1573: }
1574: return 0;
1575: }
1576: if (iTailZone == null) {
1577: return iStandardOffsets[i - 1];
1578: }
1579: return iTailZone.getStandardOffset(instant);
1580: }
1581:
1582: public boolean isFixed() {
1583: return false;
1584: }
1585:
1586: public long nextTransition(long instant) {
1587: long[] transitions = iTransitions;
1588: int i = Arrays.binarySearch(transitions, instant);
1589: i = (i >= 0) ? (i + 1) : ~i;
1590: if (i < transitions.length) {
1591: return transitions[i];
1592: }
1593: if (iTailZone == null) {
1594: return instant;
1595: }
1596: long end = transitions[transitions.length - 1];
1597: if (instant < end) {
1598: instant = end;
1599: }
1600: return iTailZone.nextTransition(instant);
1601: }
1602:
1603: public long previousTransition(long instant) {
1604: long[] transitions = iTransitions;
1605: int i = Arrays.binarySearch(transitions, instant);
1606: if (i >= 0) {
1607: if (instant > Long.MIN_VALUE) {
1608: return instant - 1;
1609: }
1610: return instant;
1611: }
1612: i = ~i;
1613: if (i < transitions.length) {
1614: if (i > 0) {
1615: long prev = transitions[i - 1];
1616: if (prev > Long.MIN_VALUE) {
1617: return prev - 1;
1618: }
1619: }
1620: return instant;
1621: }
1622: if (iTailZone != null) {
1623: long prev = iTailZone.previousTransition(instant);
1624: if (prev < instant) {
1625: return prev;
1626: }
1627: }
1628: long prev = transitions[i - 1];
1629: if (prev > Long.MIN_VALUE) {
1630: return prev - 1;
1631: }
1632: return instant;
1633: }
1634:
1635: public boolean equals(Object obj) {
1636: if (this == obj) {
1637: return true;
1638: }
1639: if (obj instanceof PrecalculatedZone) {
1640: PrecalculatedZone other = (PrecalculatedZone) obj;
1641: return getID().equals(other.getID())
1642: && Arrays.equals(iTransitions,
1643: other.iTransitions)
1644: && Arrays.equals(iNameKeys, other.iNameKeys)
1645: && Arrays.equals(iWallOffsets,
1646: other.iWallOffsets)
1647: && Arrays.equals(iStandardOffsets,
1648: other.iStandardOffsets)
1649: && ((iTailZone == null) ? (null == other.iTailZone)
1650: : (iTailZone.equals(other.iTailZone)));
1651: }
1652: return false;
1653: }
1654:
1655: public void writeTo(DataOutput out) throws IOException {
1656: int size = iTransitions.length;
1657:
1658: // Create unique string pool.
1659: Set poolSet = new HashSet();
1660: for (int i = 0; i < size; i++) {
1661: poolSet.add(iNameKeys[i]);
1662: }
1663:
1664: int poolSize = poolSet.size();
1665: if (poolSize > 65535) {
1666: throw new UnsupportedOperationException(
1667: "String pool is too large");
1668: }
1669: String[] pool = new String[poolSize];
1670: Iterator it = poolSet.iterator();
1671: for (int i = 0; it.hasNext(); i++) {
1672: pool[i] = (String) it.next();
1673: }
1674:
1675: // Write out the pool.
1676: out.writeShort(poolSize);
1677: for (int i = 0; i < poolSize; i++) {
1678: out.writeUTF(pool[i]);
1679: }
1680:
1681: out.writeInt(size);
1682:
1683: for (int i = 0; i < size; i++) {
1684: writeMillis(out, iTransitions[i]);
1685: writeMillis(out, iWallOffsets[i]);
1686: writeMillis(out, iStandardOffsets[i]);
1687:
1688: // Find pool index and write it out.
1689: String nameKey = iNameKeys[i];
1690: for (int j = 0; j < poolSize; j++) {
1691: if (pool[j].equals(nameKey)) {
1692: if (poolSize < 256) {
1693: out.writeByte(j);
1694: } else {
1695: out.writeShort(j);
1696: }
1697: break;
1698: }
1699: }
1700: }
1701:
1702: out.writeBoolean(iTailZone != null);
1703: if (iTailZone != null) {
1704: iTailZone.writeTo(out);
1705: }
1706: }
1707:
1708: public boolean isCachable() {
1709: if (iTailZone != null) {
1710: return true;
1711: }
1712: long[] transitions = iTransitions;
1713: if (transitions.length <= 1) {
1714: return false;
1715: }
1716:
1717: // Add up all the distances between transitions that are less than
1718: // about two years.
1719: double distances = 0;
1720: int count = 0;
1721:
1722: for (int i = 1; i < transitions.length; i++) {
1723: long diff = transitions[i] - transitions[i - 1];
1724: if (diff < ((366L + 365) * 24 * 60 * 60 * 1000)) {
1725: distances += (double) diff;
1726: count++;
1727: }
1728: }
1729:
1730: if (count > 0) {
1731: double avg = distances / count;
1732: avg /= 24 * 60 * 60 * 1000;
1733: if (avg >= 25) {
1734: // Only bother caching if average distance between
1735: // transitions is at least 25 days. Why 25?
1736: // CachedDateTimeZone is more efficient if the distance
1737: // between transitions is large. With an average of 25, it
1738: // will on average perform about 2 tests per cache
1739: // hit. (49.7 / 25) is approximately 2.
1740: return true;
1741: }
1742: }
1743:
1744: return false;
1745: }
1746: }
1747: }
|