All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.joda.time.tz.DateTimeZoneBuilder Maven / Gradle / Ivy

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

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

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

/**
 * DateTimeZoneBuilder allows complex DateTimeZones to be constructed. Since
 * creating a new DateTimeZone this way is a relatively expensive operation,
 * built zones can be written to a file. Reading back the encoded data is a
 * quick operation.
 * 

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

* It is intended that {@link ZoneInfoCompiler} be used to read time zone data * files, indirectly calling DateTimeZoneBuilder. The following complex * example defines the America/Los_Angeles time zone, with all historical * transitions: * *

 * 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 Brian S O'Neill * @see ZoneInfoCompiler * @see ZoneInfoProvider * @since 1.0 */ public class DateTimeZoneBuilder { /** * Decodes a built DateTimeZone from the given stream, as encoded by * writeTo. * * @param in input stream to read encoded DateTimeZone from. * @param id time zone id to assign */ public static DateTimeZone readFrom(InputStream in, String id) throws IOException { if (in instanceof DataInput) { return readFrom((DataInput)in, id); } else { return readFrom((DataInput)new DataInputStream(in), id); } } /** * Decodes a built DateTimeZone from the given stream, as encoded by * writeTo. * * @param in input stream to read encoded DateTimeZone from. * @param id time zone id to assign */ public static DateTimeZone readFrom(DataInput in, String id) throws IOException { switch (in.readUnsignedByte()) { case 'F': DateTimeZone fixed = new FixedDateTimeZone (id, in.readUTF(), (int)readMillis(in), (int)readMillis(in)); if (fixed.equals(DateTimeZone.UTC)) { fixed = DateTimeZone.UTC; } return fixed; case 'C': return CachedDateTimeZone.forZone(PrecalculatedZone.readFrom(in, id)); case 'P': return PrecalculatedZone.readFrom(in, id); default: throw new IOException("Invalid encoding"); } } /** * Millisecond encoding formats: * * upper two bits units field length approximate range * --------------------------------------------------------------- * 00 30 minutes 1 byte +/- 16 hours * 01 minutes 4 bytes +/- 1020 years * 10 seconds 5 bytes +/- 4355 years * 11 millis 9 bytes +/- 292,000,000 years * * Remaining bits in field form signed offset from 1970-01-01T00:00:00Z. */ static void writeMillis(DataOutput out, long millis) throws IOException { if (millis % (30 * 60000L) == 0) { // Try to write in 30 minute units. long units = millis / (30 * 60000L); if (((units << (64 - 6)) >> (64 - 6)) == units) { // Form 00 (6 bits effective precision) out.writeByte((int)(units & 0x3f)); return; } } if (millis % 60000L == 0) { // Try to write minutes. long minutes = millis / 60000L; if (((minutes << (64 - 30)) >> (64 - 30)) == minutes) { // Form 01 (30 bits effective precision) out.writeInt(0x40000000 | (int)(minutes & 0x3fffffff)); return; } } if (millis % 1000L == 0) { // Try to write seconds. long seconds = millis / 1000L; if (((seconds << (64 - 38)) >> (64 - 38)) == seconds) { // Form 10 (38 bits effective precision) out.writeByte(0x80 | (int)((seconds >> 32) & 0x3f)); out.writeInt((int)(seconds & 0xffffffff)); return; } } // Write milliseconds either because the additional precision is // required or the minutes didn't fit in the field. // Form 11 (64-bits effective precision, but write as if 70 bits) out.writeByte(millis < 0 ? 0xff : 0xc0); out.writeLong(millis); } /** * Reads encoding generated by writeMillis. */ static long readMillis(DataInput in) throws IOException { int v = in.readUnsignedByte(); switch (v >> 6) { case 0: default: // Form 00 (6 bits effective precision) v = (v << (32 - 6)) >> (32 - 6); return v * (30 * 60000L); case 1: // Form 01 (30 bits effective precision) v = (v << (32 - 6)) >> (32 - 30); v |= (in.readUnsignedByte()) << 16; v |= (in.readUnsignedByte()) << 8; v |= (in.readUnsignedByte()); return v * 60000L; case 2: // Form 10 (38 bits effective precision) long w = (((long)v) << (64 - 6)) >> (64 - 38); w |= (in.readUnsignedByte()) << 24; w |= (in.readUnsignedByte()) << 16; w |= (in.readUnsignedByte()) << 8; w |= (in.readUnsignedByte()); return w * 1000L; case 3: // Form 11 (64-bits effective precision) return in.readLong(); } } private static DateTimeZone buildFixedZone(String id, String nameKey, int wallOffset, int standardOffset) { if ("UTC".equals(id) && id.equals(nameKey) && wallOffset == 0 && standardOffset == 0) { return DateTimeZone.UTC; } return new FixedDateTimeZone(id, nameKey, wallOffset, standardOffset); } // List of RuleSets. private final ArrayList iRuleSets; public DateTimeZoneBuilder() { iRuleSets = new ArrayList(10); } /** * Adds a cutover for added rules. The standard offset at the cutover * defaults to 0. Call setStandardOffset afterwards to change it. * * @param year the year of cutover * @param mode 'u' - cutover is measured against UTC, 'w' - against wall * offset, 's' - against standard offset * @param monthOfYear the month from 1 (January) to 12 (December) * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). * For example, if -1, set to last day of month * @param dayOfWeek from 1 (Monday) to 7 (Sunday), if 0 then ignore * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to * dayOfWeek when true, retreat when false. * @param millisOfDay additional precision for specifying time of day of cutover */ public DateTimeZoneBuilder addCutover(int year, char mode, int monthOfYear, int dayOfMonth, int dayOfWeek, boolean advanceDayOfWeek, int millisOfDay) { if (iRuleSets.size() > 0) { OfYear ofYear = new OfYear (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay); RuleSet lastRuleSet = iRuleSets.get(iRuleSets.size() - 1); lastRuleSet.setUpperLimit(year, ofYear); } iRuleSets.add(new RuleSet()); return this; } /** * Sets the standard offset to use for newly added rules until the next * cutover is added. * @param standardOffset the standard offset in millis */ public DateTimeZoneBuilder setStandardOffset(int standardOffset) { getLastRuleSet().setStandardOffset(standardOffset); return this; } /** * Set a fixed savings rule at the cutover. */ public DateTimeZoneBuilder setFixedSavings(String nameKey, int saveMillis) { getLastRuleSet().setFixedSavings(nameKey, saveMillis); return this; } /** * Add a recurring daylight saving time rule. * * @param nameKey the name key of new rule * @param saveMillis the milliseconds to add to standard offset * @param fromYear the first year that rule is in effect, MIN_VALUE indicates * beginning of time * @param toYear the last year (inclusive) that rule is in effect, MAX_VALUE * indicates end of time * @param mode 'u' - transitions are calculated against UTC, 'w' - * transitions are calculated against wall offset, 's' - transitions are * calculated against standard offset * @param monthOfYear the month from 1 (January) to 12 (December) * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). * For example, if -1, set to last day of month * @param dayOfWeek from 1 (Monday) to 7 (Sunday), if 0 then ignore * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to * dayOfWeek when true, retreat when false. * @param millisOfDay additional precision for specifying time of day of transitions */ public DateTimeZoneBuilder addRecurringSavings(String nameKey, int saveMillis, int fromYear, int toYear, char mode, int monthOfYear, int dayOfMonth, int dayOfWeek, boolean advanceDayOfWeek, int millisOfDay) { if (fromYear <= toYear) { OfYear ofYear = new OfYear (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay); Recurrence recurrence = new Recurrence(ofYear, nameKey, saveMillis); Rule rule = new Rule(recurrence, fromYear, toYear); getLastRuleSet().addRule(rule); } return this; } private RuleSet getLastRuleSet() { if (iRuleSets.size() == 0) { addCutover(Integer.MIN_VALUE, 'w', 1, 1, 0, false, 0); } return iRuleSets.get(iRuleSets.size() - 1); } /** * Processes all the rules and builds a DateTimeZone. * * @param id time zone id to assign * @param outputID true if the zone id should be output */ public DateTimeZone toDateTimeZone(String id, boolean outputID) { if (id == null) { throw new IllegalArgumentException(); } // Discover where all the transitions occur and store the results in // these lists. ArrayList transitions = new ArrayList(); // Tail zone picks up remaining transitions in the form of an endless // DST cycle. DSTZone tailZone = null; long millis = Long.MIN_VALUE; int saveMillis = 0; int ruleSetCount = iRuleSets.size(); for (int i=0; i transitions, Transition tr) { int size = transitions.size(); if (size == 0) { // System.out.println("Adding " + tr); transitions.add(tr); return true; } Transition last = transitions.get(size - 1); if (!tr.isTransitionFrom(last)) { // System.out.println("Rejected " + tr); 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); // System.out.println("Adding " + tr); return true; } Transition previous = transitions.remove(size - 1); Transition adjusted = tr.withMillis(previous.getMillis()); // System.out.println("Current " + tr); // System.out.println("Previous " + previous); // System.out.println("Adjusted " + adjusted); return addTransition(transitions, adjusted); } /** * Encodes a built DateTimeZone to the given stream. Call readFrom to * decode the data into a DateTimeZone object. * * @param out the output stream to receive the encoded DateTimeZone * @since 1.5 (parameter added) */ public void writeTo(String zoneID, OutputStream out) throws IOException { if (out instanceof DataOutput) { writeTo(zoneID, (DataOutput)out); } else { DataOutputStream dout = new DataOutputStream(out); writeTo(zoneID, (DataOutput)dout); dout.flush(); } } /** * Encodes a built DateTimeZone to the given stream. Call readFrom to * decode the data into a DateTimeZone object. * * @param out the output stream to receive the encoded DateTimeZone * @since 1.5 (parameter added) */ public void writeTo(String zoneID, DataOutput out) throws IOException { // pass false so zone id is not written out DateTimeZone zone = toDateTimeZone(zoneID, false); if (zone instanceof FixedDateTimeZone) { out.writeByte('F'); // 'F' for fixed out.writeUTF(zone.getNameKey(0)); writeMillis(out, zone.getOffset(0)); writeMillis(out, zone.getStandardOffset(0)); } else { if (zone instanceof CachedDateTimeZone) { out.writeByte('C'); // 'C' for cached, precalculated zone = ((CachedDateTimeZone)zone).getUncachedZone(); } else { out.writeByte('P'); // 'P' for precalculated, uncached } ((PrecalculatedZone)zone).writeTo(out); } } /** * Supports setting fields of year and moving between transitions. */ private static final class OfYear { static OfYear readFrom(DataInput in) throws IOException { return new OfYear((char)in.readUnsignedByte(), (int)in.readUnsignedByte(), (int)in.readByte(), (int)in.readUnsignedByte(), in.readBoolean(), (int)readMillis(in)); } // Is 'u', 'w', or 's'. final char iMode; final int iMonthOfYear; final int iDayOfMonth; final int iDayOfWeek; final boolean iAdvance; final int iMillisOfDay; OfYear(char mode, int monthOfYear, int dayOfMonth, int dayOfWeek, boolean advanceDayOfWeek, int millisOfDay) { if (mode != 'u' && mode != 'w' && mode != 's') { throw new IllegalArgumentException("Unknown mode: " + mode); } iMode = mode; iMonthOfYear = monthOfYear; iDayOfMonth = dayOfMonth; iDayOfWeek = dayOfWeek; iAdvance = advanceDayOfWeek; iMillisOfDay = millisOfDay; } /** * @param standardOffset standard offset just before instant */ public long setInstant(int year, int standardOffset, int saveMillis) { int offset; if (iMode == 'w') { offset = standardOffset + saveMillis; } else if (iMode == 's') { offset = standardOffset; } else { offset = 0; } Chronology chrono = ISOChronology.getInstanceUTC(); long millis = chrono.year().set(0, year); millis = chrono.monthOfYear().set(millis, iMonthOfYear); millis = chrono.millisOfDay().set(millis, iMillisOfDay); millis = setDayOfMonth(chrono, millis); if (iDayOfWeek != 0) { millis = setDayOfWeek(chrono, millis); } // Convert from local time to UTC. return millis - offset; } /** * @param standardOffset standard offset just before next recurrence */ public long next(long instant, int standardOffset, int saveMillis) { int offset; if (iMode == 'w') { offset = standardOffset + saveMillis; } else if (iMode == 's') { offset = standardOffset; } else { offset = 0; } // Convert from UTC to local time. instant += offset; Chronology chrono = ISOChronology.getInstanceUTC(); long next = chrono.monthOfYear().set(instant, iMonthOfYear); // Be lenient with millisOfDay. next = chrono.millisOfDay().set(next, 0); next = chrono.millisOfDay().add(next, iMillisOfDay); next = setDayOfMonthNext(chrono, next); if (iDayOfWeek == 0) { if (next <= instant) { next = chrono.year().add(next, 1); next = setDayOfMonthNext(chrono, next); } } else { next = setDayOfWeek(chrono, next); if (next <= instant) { next = chrono.year().add(next, 1); next = chrono.monthOfYear().set(next, iMonthOfYear); next = setDayOfMonthNext(chrono, next); next = setDayOfWeek(chrono, next); } } // Convert from local time to UTC. return next - offset; } /** * @param standardOffset standard offset just before previous recurrence */ public long previous(long instant, int standardOffset, int saveMillis) { int offset; if (iMode == 'w') { offset = standardOffset + saveMillis; } else if (iMode == 's') { offset = standardOffset; } else { offset = 0; } // Convert from UTC to local time. instant += offset; Chronology chrono = ISOChronology.getInstanceUTC(); long prev = chrono.monthOfYear().set(instant, iMonthOfYear); // Be lenient with millisOfDay. prev = chrono.millisOfDay().set(prev, 0); prev = chrono.millisOfDay().add(prev, iMillisOfDay); prev = setDayOfMonthPrevious(chrono, prev); if (iDayOfWeek == 0) { if (prev >= instant) { prev = chrono.year().add(prev, -1); prev = setDayOfMonthPrevious(chrono, prev); } } else { prev = setDayOfWeek(chrono, prev); if (prev >= instant) { prev = chrono.year().add(prev, -1); prev = chrono.monthOfYear().set(prev, iMonthOfYear); prev = setDayOfMonthPrevious(chrono, prev); prev = setDayOfWeek(chrono, prev); } } // Convert from local time to UTC. return prev - offset; } public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof OfYear) { OfYear other = (OfYear)obj; return iMode == other.iMode && iMonthOfYear == other.iMonthOfYear && iDayOfMonth == other.iDayOfMonth && iDayOfWeek == other.iDayOfWeek && iAdvance == other.iAdvance && iMillisOfDay == other.iMillisOfDay; } return false; } public String toString() { return "[OfYear]\n" + "Mode: " + iMode + '\n' + "MonthOfYear: " + iMonthOfYear + '\n' + "DayOfMonth: " + iDayOfMonth + '\n' + "DayOfWeek: " + iDayOfWeek + '\n' + "AdvanceDayOfWeek: " + iAdvance + '\n' + "MillisOfDay: " + iMillisOfDay + '\n'; } public void writeTo(DataOutput out) throws IOException { out.writeByte(iMode); out.writeByte(iMonthOfYear); out.writeByte(iDayOfMonth); out.writeByte(iDayOfWeek); out.writeBoolean(iAdvance); writeMillis(out, iMillisOfDay); } /** * If month-day is 02-29 and year isn't leap, advances to next leap year. */ private long setDayOfMonthNext(Chronology chrono, long next) { try { next = setDayOfMonth(chrono, next); } catch (IllegalArgumentException e) { if (iMonthOfYear == 2 && iDayOfMonth == 29) { while (chrono.year().isLeap(next) == false) { next = chrono.year().add(next, 1); } next = setDayOfMonth(chrono, next); } else { throw e; } } return next; } /** * If month-day is 02-29 and year isn't leap, retreats to previous leap year. */ private long setDayOfMonthPrevious(Chronology chrono, long prev) { try { prev = setDayOfMonth(chrono, prev); } catch (IllegalArgumentException e) { if (iMonthOfYear == 2 && iDayOfMonth == 29) { while (chrono.year().isLeap(prev) == false) { prev = chrono.year().add(prev, -1); } prev = setDayOfMonth(chrono, prev); } else { throw e; } } return prev; } private long setDayOfMonth(Chronology chrono, long instant) { if (iDayOfMonth >= 0) { instant = chrono.dayOfMonth().set(instant, iDayOfMonth); } else { instant = chrono.dayOfMonth().set(instant, 1); instant = chrono.monthOfYear().add(instant, 1); instant = chrono.dayOfMonth().add(instant, iDayOfMonth); } return instant; } private long setDayOfWeek(Chronology chrono, long instant) { int dayOfWeek = chrono.dayOfWeek().get(instant); int daysToAdd = iDayOfWeek - dayOfWeek; if (daysToAdd != 0) { if (iAdvance) { if (daysToAdd < 0) { daysToAdd += 7; } } else { if (daysToAdd > 0) { daysToAdd -= 7; } } instant = chrono.dayOfWeek().add(instant, daysToAdd); } return instant; } } /** * Extends OfYear with a nameKey and savings. */ private static final class Recurrence { static Recurrence readFrom(DataInput in) throws IOException { return new Recurrence(OfYear.readFrom(in), in.readUTF(), (int)readMillis(in)); } final OfYear iOfYear; final String iNameKey; final int iSaveMillis; Recurrence(OfYear ofYear, String nameKey, int saveMillis) { iOfYear = ofYear; iNameKey = nameKey; iSaveMillis = saveMillis; } public OfYear getOfYear() { return iOfYear; } /** * @param standardOffset standard offset just before next recurrence */ public long next(long instant, int standardOffset, int saveMillis) { return iOfYear.next(instant, standardOffset, saveMillis); } /** * @param standardOffset standard offset just before previous recurrence */ public long previous(long instant, int standardOffset, int saveMillis) { return iOfYear.previous(instant, standardOffset, saveMillis); } public String getNameKey() { return iNameKey; } public int getSaveMillis() { return iSaveMillis; } public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Recurrence) { Recurrence other = (Recurrence)obj; return iSaveMillis == other.iSaveMillis && iNameKey.equals(other.iNameKey) && iOfYear.equals(other.iOfYear); } return false; } public void writeTo(DataOutput out) throws IOException { iOfYear.writeTo(out); out.writeUTF(iNameKey); writeMillis(out, iSaveMillis); } Recurrence rename(String nameKey) { return new Recurrence(iOfYear, nameKey, iSaveMillis); } Recurrence renameAppend(String appendNameKey) { return rename((iNameKey + appendNameKey).intern()); } @Override public String toString() { return iOfYear + " named " + iNameKey + " at " + iSaveMillis; } } /** * Extends Recurrence with inclusive year limits. */ private static final class Rule { final Recurrence iRecurrence; final int iFromYear; // inclusive final int iToYear; // inclusive Rule(Recurrence recurrence, int fromYear, int toYear) { iRecurrence = recurrence; iFromYear = fromYear; iToYear = toYear; } @SuppressWarnings("unused") public int getFromYear() { return iFromYear; } public int getToYear() { return iToYear; } @SuppressWarnings("unused") public OfYear getOfYear() { return iRecurrence.getOfYear(); } public String getNameKey() { return iRecurrence.getNameKey(); } public int getSaveMillis() { return iRecurrence.getSaveMillis(); } public long next(final long instant, int standardOffset, int saveMillis) { Chronology chrono = ISOChronology.getInstanceUTC(); final int wallOffset = standardOffset + saveMillis; long testInstant = instant; int year; if (instant == Long.MIN_VALUE) { year = Integer.MIN_VALUE; } else { year = chrono.year().get(instant + wallOffset); } if (year < iFromYear) { // First advance instant to start of from year. testInstant = chrono.year().set(0, iFromYear) - wallOffset; // Back off one millisecond to account for next recurrence // being exactly at the beginning of the year. testInstant -= 1; } long next = iRecurrence.next(testInstant, standardOffset, saveMillis); if (next > instant) { year = chrono.year().get(next + wallOffset); if (year > iToYear) { // Out of range, return original value. next = instant; } } return next; } @Override public String toString() { return iFromYear + " to " + iToYear + " using " + iRecurrence; } } private static final class Transition { private final long iMillis; private final String iNameKey; private final int iWallOffset; private final int iStandardOffset; Transition(long millis, Transition tr) { iMillis = millis; iNameKey = tr.iNameKey; iWallOffset = tr.iWallOffset; iStandardOffset = tr.iStandardOffset; } Transition(long millis, Rule rule, int standardOffset) { iMillis = millis; iNameKey = rule.getNameKey(); iWallOffset = standardOffset + rule.getSaveMillis(); iStandardOffset = standardOffset; } Transition(long millis, String nameKey, int wallOffset, int standardOffset) { iMillis = millis; iNameKey = nameKey; iWallOffset = wallOffset; iStandardOffset = standardOffset; } public long getMillis() { return iMillis; } public String getNameKey() { return iNameKey; } public int getWallOffset() { return iWallOffset; } public int getStandardOffset() { return iStandardOffset; } public int getSaveMillis() { return iWallOffset - iStandardOffset; } public Transition withMillis(long millis) { return new Transition(millis, iNameKey, iWallOffset, iStandardOffset); } /** * There must be a change in the millis, wall offsets or name keys. */ public boolean isTransitionFrom(Transition other) { if (other == null) { return true; } return iMillis > other.iMillis && (iWallOffset != other.iWallOffset || iStandardOffset != other.iStandardOffset || !(iNameKey.equals(other.iNameKey))); } @Override public String toString() { return new DateTime(iMillis, DateTimeZone.UTC) + " " + iStandardOffset + " " + iWallOffset; } } private static final class RuleSet { private static final int YEAR_LIMIT; static { // Don't pre-calculate more than 100 years into the future. Almost // all zones will stop pre-calculating far sooner anyhow. Either a // simple DST cycle is detected or the last rule is a fixed // offset. If a zone has a fixed offset set more than 100 years // into the future, then it won't be observed. long now = DateTimeUtils.currentTimeMillis(); YEAR_LIMIT = ISOChronology.getInstanceUTC().year().get(now) + 100; } private int iStandardOffset; private ArrayList iRules; // Optional. private String iInitialNameKey; private int iInitialSaveMillis; // Upper limit is exclusive. private int iUpperYear; private OfYear iUpperOfYear; RuleSet() { iRules = new ArrayList(10); iUpperYear = Integer.MAX_VALUE; } /** * Copy constructor. */ RuleSet(RuleSet rs) { iStandardOffset = rs.iStandardOffset; iRules = new ArrayList(rs.iRules); iInitialNameKey = rs.iInitialNameKey; iInitialSaveMillis = rs.iInitialSaveMillis; iUpperYear = rs.iUpperYear; iUpperOfYear = rs.iUpperOfYear; } @SuppressWarnings("unused") public int getStandardOffset() { return iStandardOffset; } public void setStandardOffset(int standardOffset) { iStandardOffset = standardOffset; } public void setFixedSavings(String nameKey, int saveMillis) { iInitialNameKey = nameKey; iInitialSaveMillis = saveMillis; } public void addRule(Rule rule) { if (!iRules.contains(rule)) { iRules.add(rule); } } public void setUpperLimit(int year, OfYear ofYear) { iUpperYear = year; iUpperOfYear = ofYear; } /** * Returns a transition at firstMillis with the first name key and * offsets for this rule set. This method may return null. * * @param firstMillis millis of first transition */ public Transition firstTransition(final long firstMillis) { if (iInitialNameKey != null) { // Initial zone info explicitly set, so don't search the rules. return new Transition(firstMillis, iInitialNameKey, iStandardOffset + iInitialSaveMillis, iStandardOffset); } // Make a copy before we destroy the rules. ArrayList copy = new ArrayList(iRules); // Iterate through all the transitions until firstMillis is // reached. Use the name key and savings for whatever rule reaches // the limit. long millis = Long.MIN_VALUE; int saveMillis = 0; Transition first = null; Transition next; while ((next = nextTransition(millis, saveMillis)) != null) { millis = next.getMillis(); if (millis == firstMillis) { first = new Transition(firstMillis, next); break; } if (millis > firstMillis) { if (first == null) { // Find first rule without savings. This way a more // accurate nameKey is found even though no rule // extends to the RuleSet's lower limit. for (Rule rule : copy) { if (rule.getSaveMillis() == 0) { first = new Transition(firstMillis, rule, iStandardOffset); break; } } } if (first == null) { // Found no rule without savings. Create a transition // with no savings anyhow, and use the best available // name key. first = new Transition(firstMillis, next.getNameKey(), iStandardOffset, iStandardOffset); } break; } // Set first to the best transition found so far, but next // iteration may find something closer to lower limit. first = new Transition(firstMillis, next); saveMillis = next.getSaveMillis(); } iRules = copy; return first; } /** * Returns null if RuleSet is exhausted or upper limit reached. Calling * this method will throw away rules as they each become * exhausted. Copy the RuleSet before using it to compute transitions. * * Returned transition may be a duplicate from previous * transition. Caller must call isTransitionFrom to filter out * duplicates. * * @param saveMillis savings before next transition */ public Transition nextTransition(final long instant, final int saveMillis) { Chronology chrono = ISOChronology.getInstanceUTC(); // Find next matching rule. Rule nextRule = null; long nextMillis = Long.MAX_VALUE; Iterator it = iRules.iterator(); while (it.hasNext()) { Rule rule = it.next(); long next = rule.next(instant, iStandardOffset, saveMillis); if (next <= instant) { it.remove(); continue; } // Even if next is same as previous next, choose the rule // in order for more recently added rules to override. if (next <= nextMillis) { // Found a better match. nextRule = rule; nextMillis = next; } } if (nextRule == null) { return null; } // Stop precalculating if year reaches some arbitrary limit. if (chrono.year().get(nextMillis) >= YEAR_LIMIT) { return null; } // Check if upper limit reached or passed. if (iUpperYear < Integer.MAX_VALUE) { long upperMillis = iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis); if (nextMillis >= upperMillis) { // At or after upper limit. return null; } } return new Transition(nextMillis, nextRule, iStandardOffset); } /** * @param saveMillis savings before upper limit */ public long getUpperLimit(int saveMillis) { if (iUpperYear == Integer.MAX_VALUE) { return Long.MAX_VALUE; } return iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis); } /** * Returns null if none can be built. */ public DSTZone buildTailZone(String id) { if (iRules.size() == 2) { Rule startRule = iRules.get(0); Rule endRule = iRules.get(1); if (startRule.getToYear() == Integer.MAX_VALUE && endRule.getToYear() == Integer.MAX_VALUE) { // With exactly two infinitely recurring rules left, a // simple DSTZone can be formed. // The order of rules can come in any order, and it doesn't // really matter which rule was chosen the 'start' and // which is chosen the 'end'. DSTZone works properly either // way. return new DSTZone(id, iStandardOffset, startRule.iRecurrence, endRule.iRecurrence); } } return null; } @Override public String toString() { return iInitialNameKey + " initial: " + iInitialSaveMillis + " std: " + iStandardOffset + " upper: " + iUpperYear + " " + iUpperOfYear + " " + iRules; } } private static final class DSTZone extends DateTimeZone { private static final long serialVersionUID = 6941492635554961361L; static DSTZone readFrom(DataInput in, String id) throws IOException { return new DSTZone(id, (int)readMillis(in), Recurrence.readFrom(in), Recurrence.readFrom(in)); } final int iStandardOffset; final Recurrence iStartRecurrence; final Recurrence iEndRecurrence; DSTZone(String id, int standardOffset, Recurrence startRecurrence, Recurrence endRecurrence) { super(id); iStandardOffset = standardOffset; iStartRecurrence = startRecurrence; iEndRecurrence = endRecurrence; } public String getNameKey(long instant) { return findMatchingRecurrence(instant).getNameKey(); } public int getOffset(long instant) { return iStandardOffset + findMatchingRecurrence(instant).getSaveMillis(); } public int getStandardOffset(long instant) { return iStandardOffset; } public boolean isFixed() { return false; } public long nextTransition(long instant) { int standardOffset = iStandardOffset; Recurrence startRecurrence = iStartRecurrence; Recurrence endRecurrence = iEndRecurrence; long start, end; try { start = startRecurrence.next (instant, standardOffset, endRecurrence.getSaveMillis()); if (instant > 0 && start < 0) { // Overflowed. start = instant; } } catch (IllegalArgumentException e) { // Overflowed. start = instant; } catch (ArithmeticException e) { // Overflowed. start = instant; } try { end = endRecurrence.next (instant, standardOffset, startRecurrence.getSaveMillis()); if (instant > 0 && end < 0) { // Overflowed. end = instant; } } catch (IllegalArgumentException e) { // Overflowed. end = instant; } catch (ArithmeticException e) { // Overflowed. end = instant; } return (start > end) ? end : start; } public long previousTransition(long instant) { // Increment in order to handle the case where instant is exactly at // a transition. instant++; int standardOffset = iStandardOffset; Recurrence startRecurrence = iStartRecurrence; Recurrence endRecurrence = iEndRecurrence; long start, end; try { start = startRecurrence.previous (instant, standardOffset, endRecurrence.getSaveMillis()); if (instant < 0 && start > 0) { // Overflowed. start = instant; } } catch (IllegalArgumentException e) { // Overflowed. start = instant; } catch (ArithmeticException e) { // Overflowed. start = instant; } try { end = endRecurrence.previous (instant, standardOffset, startRecurrence.getSaveMillis()); if (instant < 0 && end > 0) { // Overflowed. end = instant; } } catch (IllegalArgumentException e) { // Overflowed. end = instant; } catch (ArithmeticException e) { // Overflowed. end = instant; } return ((start > end) ? start : end) - 1; } public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof DSTZone) { DSTZone other = (DSTZone)obj; return getID().equals(other.getID()) && iStandardOffset == other.iStandardOffset && iStartRecurrence.equals(other.iStartRecurrence) && iEndRecurrence.equals(other.iEndRecurrence); } return false; } public void writeTo(DataOutput out) throws IOException { writeMillis(out, iStandardOffset); iStartRecurrence.writeTo(out); iEndRecurrence.writeTo(out); } private Recurrence findMatchingRecurrence(long instant) { int standardOffset = iStandardOffset; Recurrence startRecurrence = iStartRecurrence; Recurrence endRecurrence = iEndRecurrence; long start, end; try { start = startRecurrence.next (instant, standardOffset, endRecurrence.getSaveMillis()); } catch (IllegalArgumentException e) { // Overflowed. start = instant; } catch (ArithmeticException e) { // Overflowed. start = instant; } try { end = endRecurrence.next (instant, standardOffset, startRecurrence.getSaveMillis()); } catch (IllegalArgumentException e) { // Overflowed. end = instant; } catch (ArithmeticException e) { // Overflowed. end = instant; } return (start > end) ? startRecurrence : endRecurrence; } } private static final class PrecalculatedZone extends DateTimeZone { private static final long serialVersionUID = 7811976468055766265L; static PrecalculatedZone readFrom(DataInput in, String id) throws IOException { // Read string pool. int poolSize = in.readUnsignedShort(); String[] pool = new String[poolSize]; for (int i=0; i transitions, DSTZone tailZone) { int size = transitions.size(); if (size == 0) { throw new IllegalArgumentException(); } long[] trans = new long[size]; int[] wallOffsets = new int[size]; int[] standardOffsets = new int[size]; String[] nameKeys = new String[size]; Transition last = null; for (int i=0; i 4 && p.getMonths() < 8 && curNameKey.equals(zoneNameData[2]) && curNameKey.equals(zoneNameData[4])) { if (ZoneInfoLogger.verbose()) { System.out.println("Fixing duplicate name key - " + nextNameKey); System.out.println(" - " + new DateTime(trans[i], chrono) + " - " + new DateTime(trans[i + 1], chrono)); } if (curOffset > nextOffset) { nameKeys[i] = (curNameKey + "-Summer").intern(); } else if (curOffset < nextOffset) { nameKeys[i + 1] = (nextNameKey + "-Summer").intern(); i++; } } } if (tailZone != null) { if (tailZone.iStartRecurrence.getNameKey() .equals(tailZone.iEndRecurrence.getNameKey())) { if (ZoneInfoLogger.verbose()) { System.out.println("Fixing duplicate recurrent name key - " + tailZone.iStartRecurrence.getNameKey()); } if (tailZone.iStartRecurrence.getSaveMillis() > 0) { tailZone = new DSTZone( tailZone.getID(), tailZone.iStandardOffset, tailZone.iStartRecurrence.renameAppend("-Summer"), tailZone.iEndRecurrence); } else { tailZone = new DSTZone( tailZone.getID(), tailZone.iStandardOffset, tailZone.iStartRecurrence, tailZone.iEndRecurrence.renameAppend("-Summer")); } } } return new PrecalculatedZone ((outputID ? id : ""), trans, wallOffsets, standardOffsets, nameKeys, tailZone); } // All array fields have the same length. private final long[] iTransitions; private final int[] iWallOffsets; private final int[] iStandardOffsets; private final String[] iNameKeys; private final DSTZone iTailZone; /** * Constructor used ONLY for valid input, loaded via static methods. */ private PrecalculatedZone(String id, long[] transitions, int[] wallOffsets, int[] standardOffsets, String[] nameKeys, DSTZone tailZone) { super(id); iTransitions = transitions; iWallOffsets = wallOffsets; iStandardOffsets = standardOffsets; iNameKeys = nameKeys; iTailZone = tailZone; } public String getNameKey(long instant) { long[] transitions = iTransitions; int i = Arrays.binarySearch(transitions, instant); if (i >= 0) { return iNameKeys[i]; } i = ~i; if (i < transitions.length) { if (i > 0) { return iNameKeys[i - 1]; } return "UTC"; } if (iTailZone == null) { return iNameKeys[i - 1]; } return iTailZone.getNameKey(instant); } public int getOffset(long instant) { long[] transitions = iTransitions; int i = Arrays.binarySearch(transitions, instant); if (i >= 0) { return iWallOffsets[i]; } i = ~i; if (i < transitions.length) { if (i > 0) { return iWallOffsets[i - 1]; } return 0; } if (iTailZone == null) { return iWallOffsets[i - 1]; } return iTailZone.getOffset(instant); } public int getStandardOffset(long instant) { long[] transitions = iTransitions; int i = Arrays.binarySearch(transitions, instant); if (i >= 0) { return iStandardOffsets[i]; } i = ~i; if (i < transitions.length) { if (i > 0) { return iStandardOffsets[i - 1]; } return 0; } if (iTailZone == null) { return iStandardOffsets[i - 1]; } return iTailZone.getStandardOffset(instant); } public boolean isFixed() { return false; } public long nextTransition(long instant) { long[] transitions = iTransitions; int i = Arrays.binarySearch(transitions, instant); i = (i >= 0) ? (i + 1) : ~i; if (i < transitions.length) { return transitions[i]; } if (iTailZone == null) { return instant; } long end = transitions[transitions.length - 1]; if (instant < end) { instant = end; } return iTailZone.nextTransition(instant); } public long previousTransition(long instant) { long[] transitions = iTransitions; int i = Arrays.binarySearch(transitions, instant); if (i >= 0) { if (instant > Long.MIN_VALUE) { return instant - 1; } return instant; } i = ~i; if (i < transitions.length) { if (i > 0) { long prev = transitions[i - 1]; if (prev > Long.MIN_VALUE) { return prev - 1; } } return instant; } if (iTailZone != null) { long prev = iTailZone.previousTransition(instant); if (prev < instant) { return prev; } } long prev = transitions[i - 1]; if (prev > Long.MIN_VALUE) { return prev - 1; } return instant; } public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof PrecalculatedZone) { PrecalculatedZone other = (PrecalculatedZone)obj; return getID().equals(other.getID()) && Arrays.equals(iTransitions, other.iTransitions) && Arrays.equals(iNameKeys, other.iNameKeys) && Arrays.equals(iWallOffsets, other.iWallOffsets) && Arrays.equals(iStandardOffsets, other.iStandardOffsets) && ((iTailZone == null) ? (null == other.iTailZone) : (iTailZone.equals(other.iTailZone))); } return false; } public void writeTo(DataOutput out) throws IOException { int size = iTransitions.length; // Create unique string pool. Set poolSet = new HashSet(); for (int i=0; i 65535) { throw new UnsupportedOperationException("String pool is too large"); } String[] pool = new String[poolSize]; Iterator it = poolSet.iterator(); for (int i=0; it.hasNext(); i++) { pool[i] = it.next(); } // Write out the pool. out.writeShort(poolSize); for (int i=0; i 0) { double avg = distances / count; avg /= 24 * 60 * 60 * 1000; if (avg >= 25) { // Only bother caching if average distance between // transitions is at least 25 days. Why 25? // CachedDateTimeZone is more efficient if the distance // between transitions is large. With an average of 25, it // will on average perform about 2 tests per cache // hit. (49.7 / 25) is approximately 2. return true; } } return false; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy