1 | /* |
2 | * Copyright 2001-2005 Stephen Colebourne |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | package org.joda.time.tz; |
17 | |
18 | import java.io.DataInput; |
19 | import java.io.DataInputStream; |
20 | import java.io.DataOutput; |
21 | import java.io.DataOutputStream; |
22 | import java.io.IOException; |
23 | import java.io.InputStream; |
24 | import java.io.OutputStream; |
25 | import java.text.DateFormatSymbols; |
26 | import java.util.ArrayList; |
27 | import java.util.Arrays; |
28 | import java.util.HashSet; |
29 | import java.util.Iterator; |
30 | import java.util.Locale; |
31 | import java.util.Set; |
32 | |
33 | import org.joda.time.Chronology; |
34 | import org.joda.time.DateTime; |
35 | import org.joda.time.DateTimeUtils; |
36 | import org.joda.time.DateTimeZone; |
37 | import org.joda.time.Period; |
38 | import org.joda.time.PeriodType; |
39 | import org.joda.time.chrono.ISOChronology; |
40 | |
41 | /** |
42 | * DateTimeZoneBuilder allows complex DateTimeZones to be constructed. Since |
43 | * creating a new DateTimeZone this way is a relatively expensive operation, |
44 | * built zones can be written to a file. Reading back the encoded data is a |
45 | * quick operation. |
46 | * <p> |
47 | * DateTimeZoneBuilder itself is mutable and not thread-safe, but the |
48 | * DateTimeZone objects that it builds are thread-safe and immutable. |
49 | * <p> |
50 | * It is intended that {@link ZoneInfoCompiler} be used to read time zone data |
51 | * files, indirectly calling DateTimeZoneBuilder. The following complex |
52 | * example defines the America/Los_Angeles time zone, with all historical |
53 | * transitions: |
54 | * |
55 | * <pre> |
56 | * DateTimeZone America_Los_Angeles = new DateTimeZoneBuilder() |
57 | * .addCutover(-2147483648, 'w', 1, 1, 0, false, 0) |
58 | * .setStandardOffset(-28378000) |
59 | * .setFixedSavings("LMT", 0) |
60 | * .addCutover(1883, 'w', 11, 18, 0, false, 43200000) |
61 | * .setStandardOffset(-28800000) |
62 | * .addRecurringSavings("PDT", 3600000, 1918, 1919, 'w', 3, -1, 7, false, 7200000) |
63 | * .addRecurringSavings("PST", 0, 1918, 1919, 'w', 10, -1, 7, false, 7200000) |
64 | * .addRecurringSavings("PWT", 3600000, 1942, 1942, 'w', 2, 9, 0, false, 7200000) |
65 | * .addRecurringSavings("PPT", 3600000, 1945, 1945, 'u', 8, 14, 0, false, 82800000) |
66 | * .addRecurringSavings("PST", 0, 1945, 1945, 'w', 9, 30, 0, false, 7200000) |
67 | * .addRecurringSavings("PDT", 3600000, 1948, 1948, 'w', 3, 14, 0, false, 7200000) |
68 | * .addRecurringSavings("PST", 0, 1949, 1949, 'w', 1, 1, 0, false, 7200000) |
69 | * .addRecurringSavings("PDT", 3600000, 1950, 1966, 'w', 4, -1, 7, false, 7200000) |
70 | * .addRecurringSavings("PST", 0, 1950, 1961, 'w', 9, -1, 7, false, 7200000) |
71 | * .addRecurringSavings("PST", 0, 1962, 1966, 'w', 10, -1, 7, false, 7200000) |
72 | * .addRecurringSavings("PST", 0, 1967, 2147483647, 'w', 10, -1, 7, false, 7200000) |
73 | * .addRecurringSavings("PDT", 3600000, 1967, 1973, 'w', 4, -1, 7, false, 7200000) |
74 | * .addRecurringSavings("PDT", 3600000, 1974, 1974, 'w', 1, 6, 0, false, 7200000) |
75 | * .addRecurringSavings("PDT", 3600000, 1975, 1975, 'w', 2, 23, 0, false, 7200000) |
76 | * .addRecurringSavings("PDT", 3600000, 1976, 1986, 'w', 4, -1, 7, false, 7200000) |
77 | * .addRecurringSavings("PDT", 3600000, 1987, 2147483647, 'w', 4, 1, 7, true, 7200000) |
78 | * .toDateTimeZone("America/Los_Angeles"); |
79 | * </pre> |
80 | * |
81 | * @author Brian S O'Neill |
82 | * @see ZoneInfoCompiler |
83 | * @see ZoneInfoProvider |
84 | * @since 1.0 |
85 | */ |
86 | public class DateTimeZoneBuilder { |
87 | /** |
88 | * Decodes a built DateTimeZone from the given stream, as encoded by |
89 | * writeTo. |
90 | * |
91 | * @param in input stream to read encoded DateTimeZone from. |
92 | * @param id time zone id to assign |
93 | */ |
94 | public static DateTimeZone readFrom(InputStream in, String id) throws IOException { |
95 | if (in instanceof DataInput) { |
96 | return readFrom((DataInput)in, id); |
97 | } else { |
98 | return readFrom((DataInput)new DataInputStream(in), id); |
99 | } |
100 | } |
101 | |
102 | /** |
103 | * Decodes a built DateTimeZone from the given stream, as encoded by |
104 | * writeTo. |
105 | * |
106 | * @param in input stream to read encoded DateTimeZone from. |
107 | * @param id time zone id to assign |
108 | */ |
109 | public static DateTimeZone readFrom(DataInput in, String id) throws IOException { |
110 | switch (in.readUnsignedByte()) { |
111 | case 'F': |
112 | DateTimeZone fixed = new FixedDateTimeZone |
113 | (id, in.readUTF(), (int)readMillis(in), (int)readMillis(in)); |
114 | if (fixed.equals(DateTimeZone.UTC)) { |
115 | fixed = DateTimeZone.UTC; |
116 | } |
117 | return fixed; |
118 | case 'C': |
119 | return CachedDateTimeZone.forZone(PrecalculatedZone.readFrom(in, id)); |
120 | case 'P': |
121 | return PrecalculatedZone.readFrom(in, id); |
122 | default: |
123 | throw new IOException("Invalid encoding"); |
124 | } |
125 | } |
126 | |
127 | /** |
128 | * Millisecond encoding formats: |
129 | * |
130 | * upper two bits units field length approximate range |
131 | * --------------------------------------------------------------- |
132 | * 00 30 minutes 1 byte +/- 16 hours |
133 | * 01 minutes 4 bytes +/- 1020 years |
134 | * 10 seconds 5 bytes +/- 4355 years |
135 | * 11 millis 9 bytes +/- 292,000,000 years |
136 | * |
137 | * Remaining bits in field form signed offset from 1970-01-01T00:00:00Z. |
138 | */ |
139 | static void writeMillis(DataOutput out, long millis) throws IOException { |
140 | if (millis % (30 * 60000L) == 0) { |
141 | // Try to write in 30 minute units. |
142 | long units = millis / (30 * 60000L); |
143 | if (((units << (64 - 6)) >> (64 - 6)) == units) { |
144 | // Form 00 (6 bits effective precision) |
145 | out.writeByte((int)(units & 0x3f)); |
146 | return; |
147 | } |
148 | } |
149 | |
150 | if (millis % 60000L == 0) { |
151 | // Try to write minutes. |
152 | long minutes = millis / 60000L; |
153 | if (((minutes << (64 - 30)) >> (64 - 30)) == minutes) { |
154 | // Form 01 (30 bits effective precision) |
155 | out.writeInt(0x40000000 | (int)(minutes & 0x3fffffff)); |
156 | return; |
157 | } |
158 | } |
159 | |
160 | if (millis % 1000L == 0) { |
161 | // Try to write seconds. |
162 | long seconds = millis / 1000L; |
163 | if (((seconds << (64 - 38)) >> (64 - 38)) == seconds) { |
164 | // Form 10 (38 bits effective precision) |
165 | out.writeByte(0x80 | (int)((seconds >> 32) & 0x3f)); |
166 | out.writeInt((int)(seconds & 0xffffffff)); |
167 | return; |
168 | } |
169 | } |
170 | |
171 | // Write milliseconds either because the additional precision is |
172 | // required or the minutes didn't fit in the field. |
173 | |
174 | // Form 11 (64 bits effective precision, but write as if 70 bits) |
175 | out.writeByte(millis < 0 ? 0xff : 0xc0); |
176 | out.writeLong(millis); |
177 | } |
178 | |
179 | /** |
180 | * Reads encoding generated by writeMillis. |
181 | */ |
182 | static long readMillis(DataInput in) throws IOException { |
183 | int v = in.readUnsignedByte(); |
184 | switch (v >> 6) { |
185 | case 0: default: |
186 | // Form 00 (6 bits effective precision) |
187 | v = (v << (32 - 6)) >> (32 - 6); |
188 | return v * (30 * 60000L); |
189 | |
190 | case 1: |
191 | // Form 01 (30 bits effective precision) |
192 | v = (v << (32 - 6)) >> (32 - 30); |
193 | v |= (in.readUnsignedByte()) << 16; |
194 | v |= (in.readUnsignedByte()) << 8; |
195 | v |= (in.readUnsignedByte()); |
196 | return v * 60000L; |
197 | |
198 | case 2: |
199 | // Form 10 (38 bits effective precision) |
200 | long w = (((long)v) << (64 - 6)) >> (64 - 38); |
201 | w |= (in.readUnsignedByte()) << 24; |
202 | w |= (in.readUnsignedByte()) << 16; |
203 | w |= (in.readUnsignedByte()) << 8; |
204 | w |= (in.readUnsignedByte()); |
205 | return w * 1000L; |
206 | |
207 | case 3: |
208 | // Form 11 (64 bits effective precision) |
209 | return in.readLong(); |
210 | } |
211 | } |
212 | |
213 | private static DateTimeZone buildFixedZone(String id, String nameKey, |
214 | int wallOffset, int standardOffset) { |
215 | if ("UTC".equals(id) && id.equals(nameKey) && |
216 | wallOffset == 0 && standardOffset == 0) { |
217 | return DateTimeZone.UTC; |
218 | } |
219 | return new FixedDateTimeZone(id, nameKey, wallOffset, standardOffset); |
220 | } |
221 | |
222 | // List of RuleSets. |
223 | private final ArrayList iRuleSets; |
224 | |
225 | public DateTimeZoneBuilder() { |
226 | iRuleSets = new ArrayList(10); |
227 | } |
228 | |
229 | /** |
230 | * Adds a cutover for added rules. The standard offset at the cutover |
231 | * defaults to 0. Call setStandardOffset afterwards to change it. |
232 | * |
233 | * @param year year of cutover |
234 | * @param mode 'u' - cutover is measured against UTC, 'w' - against wall |
235 | * offset, 's' - against standard offset. |
236 | * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). |
237 | * For example, if -1, set to last day of month |
238 | * @param dayOfWeek if 0, ignore |
239 | * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to |
240 | * dayOfWeek when true, retreat when false. |
241 | * @param millisOfDay additional precision for specifying time of day of |
242 | * cutover |
243 | */ |
244 | public DateTimeZoneBuilder addCutover(int year, |
245 | char mode, |
246 | int monthOfYear, |
247 | int dayOfMonth, |
248 | int dayOfWeek, |
249 | boolean advanceDayOfWeek, |
250 | int millisOfDay) |
251 | { |
252 | OfYear ofYear = new OfYear |
253 | (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay); |
254 | if (iRuleSets.size() > 0) { |
255 | RuleSet lastRuleSet = (RuleSet)iRuleSets.get(iRuleSets.size() - 1); |
256 | lastRuleSet.setUpperLimit(year, ofYear); |
257 | } |
258 | iRuleSets.add(new RuleSet()); |
259 | return this; |
260 | } |
261 | |
262 | /** |
263 | * Sets the standard offset to use for newly added rules until the next |
264 | * cutover is added. |
265 | */ |
266 | public DateTimeZoneBuilder setStandardOffset(int standardOffset) { |
267 | getLastRuleSet().setStandardOffset(standardOffset); |
268 | return this; |
269 | } |
270 | |
271 | /** |
272 | * Set a fixed savings rule at the cutover. |
273 | */ |
274 | public DateTimeZoneBuilder setFixedSavings(String nameKey, int saveMillis) { |
275 | getLastRuleSet().setFixedSavings(nameKey, saveMillis); |
276 | return this; |
277 | } |
278 | |
279 | /** |
280 | * Add a recurring daylight saving time rule. |
281 | * |
282 | * @param nameKey name key of new rule |
283 | * @param saveMillis milliseconds to add to standard offset |
284 | * @param fromYear First year that rule is in effect. MIN_VALUE indicates |
285 | * beginning of time. |
286 | * @param toYear Last year (inclusive) that rule is in effect. MAX_VALUE |
287 | * indicates end of time. |
288 | * @param mode 'u' - transitions are calculated against UTC, 'w' - |
289 | * transitions are calculated against wall offset, 's' - transitions are |
290 | * calculated against standard offset. |
291 | * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). |
292 | * For example, if -1, set to last day of month |
293 | * @param dayOfWeek if 0, ignore |
294 | * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to |
295 | * dayOfWeek when true, retreat when false. |
296 | * @param millisOfDay additional precision for specifying time of day of |
297 | * transitions |
298 | */ |
299 | public DateTimeZoneBuilder addRecurringSavings(String nameKey, int saveMillis, |
300 | int fromYear, int toYear, |
301 | char mode, |
302 | int monthOfYear, |
303 | int dayOfMonth, |
304 | int dayOfWeek, |
305 | boolean advanceDayOfWeek, |
306 | int millisOfDay) |
307 | { |
308 | if (fromYear <= toYear) { |
309 | OfYear ofYear = new OfYear |
310 | (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay); |
311 | Recurrence recurrence = new Recurrence(ofYear, nameKey, saveMillis); |
312 | Rule rule = new Rule(recurrence, fromYear, toYear); |
313 | getLastRuleSet().addRule(rule); |
314 | } |
315 | return this; |
316 | } |
317 | |
318 | private RuleSet getLastRuleSet() { |
319 | if (iRuleSets.size() == 0) { |
320 | addCutover(Integer.MIN_VALUE, 'w', 1, 1, 0, false, 0); |
321 | } |
322 | return (RuleSet)iRuleSets.get(iRuleSets.size() - 1); |
323 | } |
324 | |
325 | /** |
326 | * Processes all the rules and builds a DateTimeZone. |
327 | * |
328 | * @param id time zone id to assign |
329 | * @param outputID true if the zone id should be output |
330 | */ |
331 | public DateTimeZone toDateTimeZone(String id, boolean outputID) { |
332 | if (id == null) { |
333 | throw new IllegalArgumentException(); |
334 | } |
335 | |
336 | // Discover where all the transitions occur and store the results in |
337 | // these lists. |
338 | ArrayList transitions = new ArrayList(); |
339 | |
340 | // Tail zone picks up remaining transitions in the form of an endless |
341 | // DST cycle. |
342 | DSTZone tailZone = null; |
343 | |
344 | long millis = Long.MIN_VALUE; |
345 | int saveMillis = 0; |
346 | |
347 | int ruleSetCount = iRuleSets.size(); |
348 | for (int i=0; i<ruleSetCount; i++) { |
349 | RuleSet rs = (RuleSet)iRuleSets.get(i); |
350 | Transition next = rs.firstTransition(millis); |
351 | if (next == null) { |
352 | continue; |
353 | } |
354 | addTransition(transitions, next); |
355 | millis = next.getMillis(); |
356 | saveMillis = next.getSaveMillis(); |
357 | |
358 | // Copy it since we're going to destroy it. |
359 | rs = new RuleSet(rs); |
360 | |
361 | while ((next = rs.nextTransition(millis, saveMillis)) != null) { |
362 | if (addTransition(transitions, next)) { |
363 | if (tailZone != null) { |
364 | // Got the extra transition before DSTZone. |
365 | break; |
366 | } |
367 | } |
368 | millis = next.getMillis(); |
369 | saveMillis = next.getSaveMillis(); |
370 | if (tailZone == null && i == ruleSetCount - 1) { |
371 | tailZone = rs.buildTailZone(id); |
372 | // If tailZone is not null, don't break out of main loop until |
373 | // at least one more transition is calculated. This ensures a |
374 | // correct 'seam' to the DSTZone. |
375 | } |
376 | } |
377 | |
378 | millis = rs.getUpperLimit(saveMillis); |
379 | } |
380 | |
381 | // Check if a simpler zone implementation can be returned. |
382 | if (transitions.size() == 0) { |
383 | if (tailZone != null) { |
384 | // This shouldn't happen, but handle just in case. |
385 | return tailZone; |
386 | } |
387 | return buildFixedZone(id, "UTC", 0, 0); |
388 | } |
389 | if (transitions.size() == 1 && tailZone == null) { |
390 | Transition tr = (Transition)transitions.get(0); |
391 | return buildFixedZone(id, tr.getNameKey(), |
392 | tr.getWallOffset(), tr.getStandardOffset()); |
393 | } |
394 | |
395 | PrecalculatedZone zone = PrecalculatedZone.create(id, outputID, transitions, tailZone); |
396 | if (zone.isCachable()) { |
397 | return CachedDateTimeZone.forZone(zone); |
398 | } |
399 | return zone; |
400 | } |
401 | |
402 | private boolean addTransition(ArrayList transitions, Transition tr) { |
403 | int size = transitions.size(); |
404 | if (size == 0) { |
405 | transitions.add(tr); |
406 | return true; |
407 | } |
408 | |
409 | Transition last = (Transition)transitions.get(size - 1); |
410 | if (!tr.isTransitionFrom(last)) { |
411 | return false; |
412 | } |
413 | |
414 | // If local time of new transition is same as last local time, just |
415 | // replace last transition with new one. |
416 | int offsetForLast = 0; |
417 | if (size >= 2) { |
418 | offsetForLast = ((Transition)transitions.get(size - 2)).getWallOffset(); |
419 | } |
420 | int offsetForNew = last.getWallOffset(); |
421 | |
422 | long lastLocal = last.getMillis() + offsetForLast; |
423 | long newLocal = tr.getMillis() + offsetForNew; |
424 | |
425 | if (newLocal != lastLocal) { |
426 | transitions.add(tr); |
427 | return true; |
428 | } |
429 | |
430 | transitions.remove(size - 1); |
431 | return addTransition(transitions, tr); |
432 | } |
433 | |
434 | /** |
435 | * Encodes a built DateTimeZone to the given stream. Call readFrom to |
436 | * decode the data into a DateTimeZone object. |
437 | * |
438 | * @param out output stream to receive encoded DateTimeZone. |
439 | * @since 1.5 (parameter added) |
440 | */ |
441 | public void writeTo(String zoneID, OutputStream out) throws IOException { |
442 | if (out instanceof DataOutput) { |
443 | writeTo(zoneID, (DataOutput)out); |
444 | } else { |
445 | writeTo(zoneID, (DataOutput)new DataOutputStream(out)); |
446 | } |
447 | } |
448 | |
449 | /** |
450 | * Encodes a built DateTimeZone to the given stream. Call readFrom to |
451 | * decode the data into a DateTimeZone object. |
452 | * |
453 | * @param out output stream to receive encoded DateTimeZone. |
454 | * @since 1.5 (parameter added) |
455 | */ |
456 | public void writeTo(String zoneID, DataOutput out) throws IOException { |
457 | // pass false so zone id is not written out |
458 | DateTimeZone zone = toDateTimeZone(zoneID, false); |
459 | |
460 | if (zone instanceof FixedDateTimeZone) { |
461 | out.writeByte('F'); // 'F' for fixed |
462 | out.writeUTF(zone.getNameKey(0)); |
463 | writeMillis(out, zone.getOffset(0)); |
464 | writeMillis(out, zone.getStandardOffset(0)); |
465 | } else { |
466 | if (zone instanceof CachedDateTimeZone) { |
467 | out.writeByte('C'); // 'C' for cached, precalculated |
468 | zone = ((CachedDateTimeZone)zone).getUncachedZone(); |
469 | } else { |
470 | out.writeByte('P'); // 'P' for precalculated, uncached |
471 | } |
472 | ((PrecalculatedZone)zone).writeTo(out); |
473 | } |
474 | } |
475 | |
476 | /** |
477 | * Supports setting fields of year and moving between transitions. |
478 | */ |
479 | private static final class OfYear { |
480 | static OfYear readFrom(DataInput in) throws IOException { |
481 | return new OfYear((char)in.readUnsignedByte(), |
482 | (int)in.readUnsignedByte(), |
483 | (int)in.readByte(), |
484 | (int)in.readUnsignedByte(), |
485 | in.readBoolean(), |
486 | (int)readMillis(in)); |
487 | } |
488 | |
489 | // Is 'u', 'w', or 's'. |
490 | final char iMode; |
491 | |
492 | final int iMonthOfYear; |
493 | final int iDayOfMonth; |
494 | final int iDayOfWeek; |
495 | final boolean iAdvance; |
496 | final int iMillisOfDay; |
497 | |
498 | OfYear(char mode, |
499 | int monthOfYear, |
500 | int dayOfMonth, |
501 | int dayOfWeek, boolean advanceDayOfWeek, |
502 | int millisOfDay) |
503 | { |
504 | if (mode != 'u' && mode != 'w' && mode != 's') { |
505 | throw new IllegalArgumentException("Unknown mode: " + mode); |
506 | } |
507 | |
508 | iMode = mode; |
509 | iMonthOfYear = monthOfYear; |
510 | iDayOfMonth = dayOfMonth; |
511 | iDayOfWeek = dayOfWeek; |
512 | iAdvance = advanceDayOfWeek; |
513 | iMillisOfDay = millisOfDay; |
514 | } |
515 | |
516 | /** |
517 | * @param standardOffset standard offset just before instant |
518 | */ |
519 | public long setInstant(int year, int standardOffset, int saveMillis) { |
520 | int offset; |
521 | if (iMode == 'w') { |
522 | offset = standardOffset + saveMillis; |
523 | } else if (iMode == 's') { |
524 | offset = standardOffset; |
525 | } else { |
526 | offset = 0; |
527 | } |
528 | |
529 | Chronology chrono = ISOChronology.getInstanceUTC(); |
530 | long millis = chrono.year().set(0, year); |
531 | millis = chrono.monthOfYear().set(millis, iMonthOfYear); |
532 | millis = chrono.millisOfDay().set(millis, iMillisOfDay); |
533 | millis = setDayOfMonth(chrono, millis); |
534 | |
535 | if (iDayOfWeek != 0) { |
536 | millis = setDayOfWeek(chrono, millis); |
537 | } |
538 | |
539 | // Convert from local time to UTC. |
540 | return millis - offset; |
541 | } |
542 | |
543 | /** |
544 | * @param standardOffset standard offset just before next recurrence |
545 | */ |
546 | public long next(long instant, int standardOffset, int saveMillis) { |
547 | int offset; |
548 | if (iMode == 'w') { |
549 | offset = standardOffset + saveMillis; |
550 | } else if (iMode == 's') { |
551 | offset = standardOffset; |
552 | } else { |
553 | offset = 0; |
554 | } |
555 | |
556 | // Convert from UTC to local time. |
557 | instant += offset; |
558 | |
559 | Chronology chrono = ISOChronology.getInstanceUTC(); |
560 | long next = chrono.monthOfYear().set(instant, iMonthOfYear); |
561 | // Be lenient with millisOfDay. |
562 | next = chrono.millisOfDay().set(next, 0); |
563 | next = chrono.millisOfDay().add(next, iMillisOfDay); |
564 | next = setDayOfMonthNext(chrono, next); |
565 | |
566 | if (iDayOfWeek == 0) { |
567 | if (next <= instant) { |
568 | next = chrono.year().add(next, 1); |
569 | next = setDayOfMonthNext(chrono, next); |
570 | } |
571 | } else { |
572 | next = setDayOfWeek(chrono, next); |
573 | if (next <= instant) { |
574 | next = chrono.year().add(next, 1); |
575 | next = chrono.monthOfYear().set(next, iMonthOfYear); |
576 | next = setDayOfMonthNext(chrono, next); |
577 | next = setDayOfWeek(chrono, next); |
578 | } |
579 | } |
580 | |
581 | // Convert from local time to UTC. |
582 | return next - offset; |
583 | } |
584 | |
585 | /** |
586 | * @param standardOffset standard offset just before previous recurrence |
587 | */ |
588 | public long previous(long instant, int standardOffset, int saveMillis) { |
589 | int offset; |
590 | if (iMode == 'w') { |
591 | offset = standardOffset + saveMillis; |
592 | } else if (iMode == 's') { |
593 | offset = standardOffset; |
594 | } else { |
595 | offset = 0; |
596 | } |
597 | |
598 | // Convert from UTC to local time. |
599 | instant += offset; |
600 | |
601 | Chronology chrono = ISOChronology.getInstanceUTC(); |
602 | long prev = chrono.monthOfYear().set(instant, iMonthOfYear); |
603 | // Be lenient with millisOfDay. |
604 | prev = chrono.millisOfDay().set(prev, 0); |
605 | prev = chrono.millisOfDay().add(prev, iMillisOfDay); |
606 | prev = setDayOfMonthPrevious(chrono, prev); |
607 | |
608 | if (iDayOfWeek == 0) { |
609 | if (prev >= instant) { |
610 | prev = chrono.year().add(prev, -1); |
611 | prev = setDayOfMonthPrevious(chrono, prev); |
612 | } |
613 | } else { |
614 | prev = setDayOfWeek(chrono, prev); |
615 | if (prev >= instant) { |
616 | prev = chrono.year().add(prev, -1); |
617 | prev = chrono.monthOfYear().set(prev, iMonthOfYear); |
618 | prev = setDayOfMonthPrevious(chrono, prev); |
619 | prev = setDayOfWeek(chrono, prev); |
620 | } |
621 | } |
622 | |
623 | // Convert from local time to UTC. |
624 | return prev - offset; |
625 | } |
626 | |
627 | public boolean equals(Object obj) { |
628 | if (this == obj) { |
629 | return true; |
630 | } |
631 | if (obj instanceof OfYear) { |
632 | OfYear other = (OfYear)obj; |
633 | return |
634 | iMode == other.iMode && |
635 | iMonthOfYear == other.iMonthOfYear && |
636 | iDayOfMonth == other.iDayOfMonth && |
637 | iDayOfWeek == other.iDayOfWeek && |
638 | iAdvance == other.iAdvance && |
639 | iMillisOfDay == other.iMillisOfDay; |
640 | } |
641 | return false; |
642 | } |
643 | |
644 | /* |
645 | public String toString() { |
646 | return |
647 | "[OfYear]\n" + |
648 | "Mode: " + iMode + '\n' + |
649 | "MonthOfYear: " + iMonthOfYear + '\n' + |
650 | "DayOfMonth: " + iDayOfMonth + '\n' + |
651 | "DayOfWeek: " + iDayOfWeek + '\n' + |
652 | "AdvanceDayOfWeek: " + iAdvance + '\n' + |
653 | "MillisOfDay: " + iMillisOfDay + '\n'; |
654 | } |
655 | */ |
656 | |
657 | public void writeTo(DataOutput out) throws IOException { |
658 | out.writeByte(iMode); |
659 | out.writeByte(iMonthOfYear); |
660 | out.writeByte(iDayOfMonth); |
661 | out.writeByte(iDayOfWeek); |
662 | out.writeBoolean(iAdvance); |
663 | writeMillis(out, iMillisOfDay); |
664 | } |
665 | |
666 | /** |
667 | * If month-day is 02-29 and year isn't leap, advances to next leap year. |
668 | */ |
669 | private long setDayOfMonthNext(Chronology chrono, long next) { |
670 | try { |
671 | next = setDayOfMonth(chrono, next); |
672 | } catch (IllegalArgumentException e) { |
673 | if (iMonthOfYear == 2 && iDayOfMonth == 29) { |
674 | while (chrono.year().isLeap(next) == false) { |
675 | next = chrono.year().add(next, 1); |
676 | } |
677 | next = setDayOfMonth(chrono, next); |
678 | } else { |
679 | throw e; |
680 | } |
681 | } |
682 | return next; |
683 | } |
684 | |
685 | /** |
686 | * If month-day is 02-29 and year isn't leap, retreats to previous leap year. |
687 | */ |
688 | private long setDayOfMonthPrevious(Chronology chrono, long prev) { |
689 | try { |
690 | prev = setDayOfMonth(chrono, prev); |
691 | } catch (IllegalArgumentException e) { |
692 | if (iMonthOfYear == 2 && iDayOfMonth == 29) { |
693 | while (chrono.year().isLeap(prev) == false) { |
694 | prev = chrono.year().add(prev, -1); |
695 | } |
696 | prev = setDayOfMonth(chrono, prev); |
697 | } else { |
698 | throw e; |
699 | } |
700 | } |
701 | return prev; |
702 | } |
703 | |
704 | private long setDayOfMonth(Chronology chrono, long instant) { |
705 | if (iDayOfMonth >= 0) { |
706 | instant = chrono.dayOfMonth().set(instant, iDayOfMonth); |
707 | } else { |
708 | instant = chrono.dayOfMonth().set(instant, 1); |
709 | instant = chrono.monthOfYear().add(instant, 1); |
710 | instant = chrono.dayOfMonth().add(instant, iDayOfMonth); |
711 | } |
712 | return instant; |
713 | } |
714 | |
715 | private long setDayOfWeek(Chronology chrono, long instant) { |
716 | int dayOfWeek = chrono.dayOfWeek().get(instant); |
717 | int daysToAdd = iDayOfWeek - dayOfWeek; |
718 | if (daysToAdd != 0) { |
719 | if (iAdvance) { |
720 | if (daysToAdd < 0) { |
721 | daysToAdd += 7; |
722 | } |
723 | } else { |
724 | if (daysToAdd > 0) { |
725 | daysToAdd -= 7; |
726 | } |
727 | } |
728 | instant = chrono.dayOfWeek().add(instant, daysToAdd); |
729 | } |
730 | return instant; |
731 | } |
732 | } |
733 | |
734 | /** |
735 | * Extends OfYear with a nameKey and savings. |
736 | */ |
737 | private static final class Recurrence { |
738 | static Recurrence readFrom(DataInput in) throws IOException { |
739 | return new Recurrence(OfYear.readFrom(in), in.readUTF(), (int)readMillis(in)); |
740 | } |
741 | |
742 | final OfYear iOfYear; |
743 | final String iNameKey; |
744 | final int iSaveMillis; |
745 | |
746 | Recurrence(OfYear ofYear, String nameKey, int saveMillis) { |
747 | iOfYear = ofYear; |
748 | iNameKey = nameKey; |
749 | iSaveMillis = saveMillis; |
750 | } |
751 | |
752 | public OfYear getOfYear() { |
753 | return iOfYear; |
754 | } |
755 | |
756 | /** |
757 | * @param standardOffset standard offset just before next recurrence |
758 | */ |
759 | public long next(long instant, int standardOffset, int saveMillis) { |
760 | return iOfYear.next(instant, standardOffset, saveMillis); |
761 | } |
762 | |
763 | /** |
764 | * @param standardOffset standard offset just before previous recurrence |
765 | */ |
766 | public long previous(long instant, int standardOffset, int saveMillis) { |
767 | return iOfYear.previous(instant, standardOffset, saveMillis); |
768 | } |
769 | |
770 | public String getNameKey() { |
771 | return iNameKey; |
772 | } |
773 | |
774 | public int getSaveMillis() { |
775 | return iSaveMillis; |
776 | } |
777 | |
778 | public boolean equals(Object obj) { |
779 | if (this == obj) { |
780 | return true; |
781 | } |
782 | if (obj instanceof Recurrence) { |
783 | Recurrence other = (Recurrence)obj; |
784 | return |
785 | iSaveMillis == other.iSaveMillis && |
786 | iNameKey.equals(other.iNameKey) && |
787 | iOfYear.equals(other.iOfYear); |
788 | } |
789 | return false; |
790 | } |
791 | |
792 | public void writeTo(DataOutput out) throws IOException { |
793 | iOfYear.writeTo(out); |
794 | out.writeUTF(iNameKey); |
795 | writeMillis(out, iSaveMillis); |
796 | } |
797 | |
798 | Recurrence rename(String nameKey) { |
799 | return new Recurrence(iOfYear, nameKey, iSaveMillis); |
800 | } |
801 | |
802 | Recurrence renameAppend(String appendNameKey) { |
803 | return rename((iNameKey + appendNameKey).intern()); |
804 | } |
805 | } |
806 | |
807 | /** |
808 | * Extends Recurrence with inclusive year limits. |
809 | */ |
810 | private static final class Rule { |
811 | final Recurrence iRecurrence; |
812 | final int iFromYear; // inclusive |
813 | final int iToYear; // inclusive |
814 | |
815 | Rule(Recurrence recurrence, int fromYear, int toYear) { |
816 | iRecurrence = recurrence; |
817 | iFromYear = fromYear; |
818 | iToYear = toYear; |
819 | } |
820 | |
821 | public int getFromYear() { |
822 | return iFromYear; |
823 | } |
824 | |
825 | public int getToYear() { |
826 | return iToYear; |
827 | } |
828 | |
829 | public OfYear getOfYear() { |
830 | return iRecurrence.getOfYear(); |
831 | } |
832 | |
833 | public String getNameKey() { |
834 | return iRecurrence.getNameKey(); |
835 | } |
836 | |
837 | public int getSaveMillis() { |
838 | return iRecurrence.getSaveMillis(); |
839 | } |
840 | |
841 | public long next(final long instant, int standardOffset, int saveMillis) { |
842 | Chronology chrono = ISOChronology.getInstanceUTC(); |
843 | |
844 | final int wallOffset = standardOffset + saveMillis; |
845 | long testInstant = instant; |
846 | |
847 | int year; |
848 | if (instant == Long.MIN_VALUE) { |
849 | year = Integer.MIN_VALUE; |
850 | } else { |
851 | year = chrono.year().get(instant + wallOffset); |
852 | } |
853 | |
854 | if (year < iFromYear) { |
855 | // First advance instant to start of from year. |
856 | testInstant = chrono.year().set(0, iFromYear) - wallOffset; |
857 | // Back off one millisecond to account for next recurrence |
858 | // being exactly at the beginning of the year. |
859 | testInstant -= 1; |
860 | } |
861 | |
862 | long next = iRecurrence.next(testInstant, standardOffset, saveMillis); |
863 | |
864 | if (next > instant) { |
865 | year = chrono.year().get(next + wallOffset); |
866 | if (year > iToYear) { |
867 | // Out of range, return original value. |
868 | next = instant; |
869 | } |
870 | } |
871 | |
872 | return next; |
873 | } |
874 | } |
875 | |
876 | private static final class Transition { |
877 | private final long iMillis; |
878 | private final String iNameKey; |
879 | private final int iWallOffset; |
880 | private final int iStandardOffset; |
881 | |
882 | Transition(long millis, Transition tr) { |
883 | iMillis = millis; |
884 | iNameKey = tr.iNameKey; |
885 | iWallOffset = tr.iWallOffset; |
886 | iStandardOffset = tr.iStandardOffset; |
887 | } |
888 | |
889 | Transition(long millis, Rule rule, int standardOffset) { |
890 | iMillis = millis; |
891 | iNameKey = rule.getNameKey(); |
892 | iWallOffset = standardOffset + rule.getSaveMillis(); |
893 | iStandardOffset = standardOffset; |
894 | } |
895 | |
896 | Transition(long millis, String nameKey, |
897 | int wallOffset, int standardOffset) { |
898 | iMillis = millis; |
899 | iNameKey = nameKey; |
900 | iWallOffset = wallOffset; |
901 | iStandardOffset = standardOffset; |
902 | } |
903 | |
904 | public long getMillis() { |
905 | return iMillis; |
906 | } |
907 | |
908 | public String getNameKey() { |
909 | return iNameKey; |
910 | } |
911 | |
912 | public int getWallOffset() { |
913 | return iWallOffset; |
914 | } |
915 | |
916 | public int getStandardOffset() { |
917 | return iStandardOffset; |
918 | } |
919 | |
920 | public int getSaveMillis() { |
921 | return iWallOffset - iStandardOffset; |
922 | } |
923 | |
924 | /** |
925 | * There must be a change in the millis, wall offsets or name keys. |
926 | */ |
927 | public boolean isTransitionFrom(Transition other) { |
928 | if (other == null) { |
929 | return true; |
930 | } |
931 | return iMillis > other.iMillis && |
932 | (iWallOffset != other.iWallOffset || |
933 | //iStandardOffset != other.iStandardOffset || |
934 | !(iNameKey.equals(other.iNameKey))); |
935 | } |
936 | } |
937 | |
938 | private static final class RuleSet { |
939 | private static final int YEAR_LIMIT; |
940 | |
941 | static { |
942 | // Don't pre-calculate more than 100 years into the future. Almost |
943 | // all zones will stop pre-calculating far sooner anyhow. Either a |
944 | // simple DST cycle is detected or the last rule is a fixed |
945 | // offset. If a zone has a fixed offset set more than 100 years |
946 | // into the future, then it won't be observed. |
947 | long now = DateTimeUtils.currentTimeMillis(); |
948 | YEAR_LIMIT = ISOChronology.getInstanceUTC().year().get(now) + 100; |
949 | } |
950 | |
951 | private int iStandardOffset; |
952 | private ArrayList iRules; |
953 | |
954 | // Optional. |
955 | private String iInitialNameKey; |
956 | private int iInitialSaveMillis; |
957 | |
958 | // Upper limit is exclusive. |
959 | private int iUpperYear; |
960 | private OfYear iUpperOfYear; |
961 | |
962 | RuleSet() { |
963 | iRules = new ArrayList(10); |
964 | iUpperYear = Integer.MAX_VALUE; |
965 | } |
966 | |
967 | /** |
968 | * Copy constructor. |
969 | */ |
970 | RuleSet(RuleSet rs) { |
971 | iStandardOffset = rs.iStandardOffset; |
972 | iRules = new ArrayList(rs.iRules); |
973 | iInitialNameKey = rs.iInitialNameKey; |
974 | iInitialSaveMillis = rs.iInitialSaveMillis; |
975 | iUpperYear = rs.iUpperYear; |
976 | iUpperOfYear = rs.iUpperOfYear; |
977 | } |
978 | |
979 | public int getStandardOffset() { |
980 | return iStandardOffset; |
981 | } |
982 | |
983 | public void setStandardOffset(int standardOffset) { |
984 | iStandardOffset = standardOffset; |
985 | } |
986 | |
987 | public void setFixedSavings(String nameKey, int saveMillis) { |
988 | iInitialNameKey = nameKey; |
989 | iInitialSaveMillis = saveMillis; |
990 | } |
991 | |
992 | public void addRule(Rule rule) { |
993 | if (!iRules.contains(rule)) { |
994 | iRules.add(rule); |
995 | } |
996 | } |
997 | |
998 | public void setUpperLimit(int year, OfYear ofYear) { |
999 | iUpperYear = year; |
1000 | iUpperOfYear = ofYear; |
1001 | } |
1002 | |
1003 | /** |
1004 | * Returns a transition at firstMillis with the first name key and |
1005 | * offsets for this rule set. This method may return null. |
1006 | * |
1007 | * @param firstMillis millis of first transition |
1008 | */ |
1009 | public Transition firstTransition(final long firstMillis) { |
1010 | if (iInitialNameKey != null) { |
1011 | // Initial zone info explicitly set, so don't search the rules. |
1012 | return new Transition(firstMillis, iInitialNameKey, |
1013 | iStandardOffset + iInitialSaveMillis, iStandardOffset); |
1014 | } |
1015 | |
1016 | // Make a copy before we destroy the rules. |
1017 | ArrayList copy = new ArrayList(iRules); |
1018 | |
1019 | // Iterate through all the transitions until firstMillis is |
1020 | // reached. Use the name key and savings for whatever rule reaches |
1021 | // the limit. |
1022 | |
1023 | long millis = Long.MIN_VALUE; |
1024 | int saveMillis = 0; |
1025 | Transition first = null; |
1026 | |
1027 | Transition next; |
1028 | while ((next = nextTransition(millis, saveMillis)) != null) { |
1029 | millis = next.getMillis(); |
1030 | |
1031 | if (millis == firstMillis) { |
1032 | first = new Transition(firstMillis, next); |
1033 | break; |
1034 | } |
1035 | |
1036 | if (millis > firstMillis) { |
1037 | if (first == null) { |
1038 | // Find first rule without savings. This way a more |
1039 | // accurate nameKey is found even though no rule |
1040 | // extends to the RuleSet's lower limit. |
1041 | Iterator it = copy.iterator(); |
1042 | while (it.hasNext()) { |
1043 | Rule rule = (Rule)it.next(); |
1044 | if (rule.getSaveMillis() == 0) { |
1045 | first = new Transition(firstMillis, rule, iStandardOffset); |
1046 | break; |
1047 | } |
1048 | } |
1049 | } |
1050 | if (first == null) { |
1051 | // Found no rule without savings. Create a transition |
1052 | // with no savings anyhow, and use the best available |
1053 | // name key. |
1054 | first = new Transition(firstMillis, next.getNameKey(), |
1055 | iStandardOffset, iStandardOffset); |
1056 | } |
1057 | break; |
1058 | } |
1059 | |
1060 | // Set first to the best transition found so far, but next |
1061 | // iteration may find something closer to lower limit. |
1062 | first = new Transition(firstMillis, next); |
1063 | |
1064 | saveMillis = next.getSaveMillis(); |
1065 | } |
1066 | |
1067 | iRules = copy; |
1068 | return first; |
1069 | } |
1070 | |
1071 | /** |
1072 | * Returns null if RuleSet is exhausted or upper limit reached. Calling |
1073 | * this method will throw away rules as they each become |
1074 | * exhausted. Copy the RuleSet before using it to compute transitions. |
1075 | * |
1076 | * Returned transition may be a duplicate from previous |
1077 | * transition. Caller must call isTransitionFrom to filter out |
1078 | * duplicates. |
1079 | * |
1080 | * @param saveMillis savings before next transition |
1081 | */ |
1082 | public Transition nextTransition(final long instant, final int saveMillis) { |
1083 | Chronology chrono = ISOChronology.getInstanceUTC(); |
1084 | |
1085 | // Find next matching rule. |
1086 | Rule nextRule = null; |
1087 | long nextMillis = Long.MAX_VALUE; |
1088 | |
1089 | Iterator it = iRules.iterator(); |
1090 | while (it.hasNext()) { |
1091 | Rule rule = (Rule)it.next(); |
1092 | long next = rule.next(instant, iStandardOffset, saveMillis); |
1093 | if (next <= instant) { |
1094 | it.remove(); |
1095 | continue; |
1096 | } |
1097 | // Even if next is same as previous next, choose the rule |
1098 | // in order for more recently added rules to override. |
1099 | if (next <= nextMillis) { |
1100 | // Found a better match. |
1101 | nextRule = rule; |
1102 | nextMillis = next; |
1103 | } |
1104 | } |
1105 | |
1106 | if (nextRule == null) { |
1107 | return null; |
1108 | } |
1109 | |
1110 | // Stop precalculating if year reaches some arbitrary limit. |
1111 | if (chrono.year().get(nextMillis) >= YEAR_LIMIT) { |
1112 | return null; |
1113 | } |
1114 | |
1115 | // Check if upper limit reached or passed. |
1116 | if (iUpperYear < Integer.MAX_VALUE) { |
1117 | long upperMillis = |
1118 | iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis); |
1119 | if (nextMillis >= upperMillis) { |
1120 | // At or after upper limit. |
1121 | return null; |
1122 | } |
1123 | } |
1124 | |
1125 | return new Transition(nextMillis, nextRule, iStandardOffset); |
1126 | } |
1127 | |
1128 | /** |
1129 | * @param saveMillis savings before upper limit |
1130 | */ |
1131 | public long getUpperLimit(int saveMillis) { |
1132 | if (iUpperYear == Integer.MAX_VALUE) { |
1133 | return Long.MAX_VALUE; |
1134 | } |
1135 | return iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis); |
1136 | } |
1137 | |
1138 | /** |
1139 | * Returns null if none can be built. |
1140 | */ |
1141 | public DSTZone buildTailZone(String id) { |
1142 | if (iRules.size() == 2) { |
1143 | Rule startRule = (Rule)iRules.get(0); |
1144 | Rule endRule = (Rule)iRules.get(1); |
1145 | if (startRule.getToYear() == Integer.MAX_VALUE && |
1146 | endRule.getToYear() == Integer.MAX_VALUE) { |
1147 | |
1148 | // With exactly two infinitely recurring rules left, a |
1149 | // simple DSTZone can be formed. |
1150 | |
1151 | // The order of rules can come in any order, and it doesn't |
1152 | // really matter which rule was chosen the 'start' and |
1153 | // which is chosen the 'end'. DSTZone works properly either |
1154 | // way. |
1155 | return new DSTZone(id, iStandardOffset, |
1156 | startRule.iRecurrence, endRule.iRecurrence); |
1157 | } |
1158 | } |
1159 | return null; |
1160 | } |
1161 | } |
1162 | |
1163 | private static final class DSTZone extends DateTimeZone { |
1164 | private static final long serialVersionUID = 6941492635554961361L; |
1165 | |
1166 | static DSTZone readFrom(DataInput in, String id) throws IOException { |
1167 | return new DSTZone(id, (int)readMillis(in), |
1168 | Recurrence.readFrom(in), Recurrence.readFrom(in)); |
1169 | } |
1170 | |
1171 | final int iStandardOffset; |
1172 | final Recurrence iStartRecurrence; |
1173 | final Recurrence iEndRecurrence; |
1174 | |
1175 | DSTZone(String id, int standardOffset, |
1176 | Recurrence startRecurrence, Recurrence endRecurrence) { |
1177 | super(id); |
1178 | iStandardOffset = standardOffset; |
1179 | iStartRecurrence = startRecurrence; |
1180 | iEndRecurrence = endRecurrence; |
1181 | } |
1182 | |
1183 | public String getNameKey(long instant) { |
1184 | return findMatchingRecurrence(instant).getNameKey(); |
1185 | } |
1186 | |
1187 | public int getOffset(long instant) { |
1188 | return iStandardOffset + findMatchingRecurrence(instant).getSaveMillis(); |
1189 | } |
1190 | |
1191 | public int getStandardOffset(long instant) { |
1192 | return iStandardOffset; |
1193 | } |
1194 | |
1195 | public boolean isFixed() { |
1196 | return false; |
1197 | } |
1198 | |
1199 | public long nextTransition(long instant) { |
1200 | int standardOffset = iStandardOffset; |
1201 | Recurrence startRecurrence = iStartRecurrence; |
1202 | Recurrence endRecurrence = iEndRecurrence; |
1203 | |
1204 | long start, end; |
1205 | |
1206 | try { |
1207 | start = startRecurrence.next |
1208 | (instant, standardOffset, endRecurrence.getSaveMillis()); |
1209 | if (instant > 0 && start < 0) { |
1210 | // Overflowed. |
1211 | start = instant; |
1212 | } |
1213 | } catch (IllegalArgumentException e) { |
1214 | // Overflowed. |
1215 | start = instant; |
1216 | } catch (ArithmeticException e) { |
1217 | // Overflowed. |
1218 | start = instant; |
1219 | } |
1220 | |
1221 | try { |
1222 | end = endRecurrence.next |
1223 | (instant, standardOffset, startRecurrence.getSaveMillis()); |
1224 | if (instant > 0 && end < 0) { |
1225 | // Overflowed. |
1226 | end = instant; |
1227 | } |
1228 | } catch (IllegalArgumentException e) { |
1229 | // Overflowed. |
1230 | end = instant; |
1231 | } catch (ArithmeticException e) { |
1232 | // Overflowed. |
1233 | end = instant; |
1234 | } |
1235 | |
1236 | return (start > end) ? end : start; |
1237 | } |
1238 | |
1239 | public long previousTransition(long instant) { |
1240 | // Increment in order to handle the case where instant is exactly at |
1241 | // a transition. |
1242 | instant++; |
1243 | |
1244 | int standardOffset = iStandardOffset; |
1245 | Recurrence startRecurrence = iStartRecurrence; |
1246 | Recurrence endRecurrence = iEndRecurrence; |
1247 | |
1248 | long start, end; |
1249 | |
1250 | try { |
1251 | start = startRecurrence.previous |
1252 | (instant, standardOffset, endRecurrence.getSaveMillis()); |
1253 | if (instant < 0 && start > 0) { |
1254 | // Overflowed. |
1255 | start = instant; |
1256 | } |
1257 | } catch (IllegalArgumentException e) { |
1258 | // Overflowed. |
1259 | start = instant; |
1260 | } catch (ArithmeticException e) { |
1261 | // Overflowed. |
1262 | start = instant; |
1263 | } |
1264 | |
1265 | try { |
1266 | end = endRecurrence.previous |
1267 | (instant, standardOffset, startRecurrence.getSaveMillis()); |
1268 | if (instant < 0 && end > 0) { |
1269 | // Overflowed. |
1270 | end = instant; |
1271 | } |
1272 | } catch (IllegalArgumentException e) { |
1273 | // Overflowed. |
1274 | end = instant; |
1275 | } catch (ArithmeticException e) { |
1276 | // Overflowed. |
1277 | end = instant; |
1278 | } |
1279 | |
1280 | return ((start > end) ? start : end) - 1; |
1281 | } |
1282 | |
1283 | public boolean equals(Object obj) { |
1284 | if (this == obj) { |
1285 | return true; |
1286 | } |
1287 | if (obj instanceof DSTZone) { |
1288 | DSTZone other = (DSTZone)obj; |
1289 | return |
1290 | getID().equals(other.getID()) && |
1291 | iStandardOffset == other.iStandardOffset && |
1292 | iStartRecurrence.equals(other.iStartRecurrence) && |
1293 | iEndRecurrence.equals(other.iEndRecurrence); |
1294 | } |
1295 | return false; |
1296 | } |
1297 | |
1298 | public void writeTo(DataOutput out) throws IOException { |
1299 | writeMillis(out, iStandardOffset); |
1300 | iStartRecurrence.writeTo(out); |
1301 | iEndRecurrence.writeTo(out); |
1302 | } |
1303 | |
1304 | private Recurrence findMatchingRecurrence(long instant) { |
1305 | int standardOffset = iStandardOffset; |
1306 | Recurrence startRecurrence = iStartRecurrence; |
1307 | Recurrence endRecurrence = iEndRecurrence; |
1308 | |
1309 | long start, end; |
1310 | |
1311 | try { |
1312 | start = startRecurrence.next |
1313 | (instant, standardOffset, endRecurrence.getSaveMillis()); |
1314 | } catch (IllegalArgumentException e) { |
1315 | // Overflowed. |
1316 | start = instant; |
1317 | } catch (ArithmeticException e) { |
1318 | // Overflowed. |
1319 | start = instant; |
1320 | } |
1321 | |
1322 | try { |
1323 | end = endRecurrence.next |
1324 | (instant, standardOffset, startRecurrence.getSaveMillis()); |
1325 | } catch (IllegalArgumentException e) { |
1326 | // Overflowed. |
1327 | end = instant; |
1328 | } catch (ArithmeticException e) { |
1329 | // Overflowed. |
1330 | end = instant; |
1331 | } |
1332 | |
1333 | return (start > end) ? startRecurrence : endRecurrence; |
1334 | } |
1335 | } |
1336 | |
1337 | private static final class PrecalculatedZone extends DateTimeZone { |
1338 | private static final long serialVersionUID = 7811976468055766265L; |
1339 | |
1340 | static PrecalculatedZone readFrom(DataInput in, String id) throws IOException { |
1341 | // Read string pool. |
1342 | int poolSize = in.readUnsignedShort(); |
1343 | String[] pool = new String[poolSize]; |
1344 | for (int i=0; i<poolSize; i++) { |
1345 | pool[i] = in.readUTF(); |
1346 | } |
1347 | |
1348 | int size = in.readInt(); |
1349 | long[] transitions = new long[size]; |
1350 | int[] wallOffsets = new int[size]; |
1351 | int[] standardOffsets = new int[size]; |
1352 | String[] nameKeys = new String[size]; |
1353 | |
1354 | for (int i=0; i<size; i++) { |
1355 | transitions[i] = readMillis(in); |
1356 | wallOffsets[i] = (int)readMillis(in); |
1357 | standardOffsets[i] = (int)readMillis(in); |
1358 | try { |
1359 | int index; |
1360 | if (poolSize < 256) { |
1361 | index = in.readUnsignedByte(); |
1362 | } else { |
1363 | index = in.readUnsignedShort(); |
1364 | } |
1365 | nameKeys[i] = pool[index]; |
1366 | } catch (ArrayIndexOutOfBoundsException e) { |
1367 | throw new IOException("Invalid encoding"); |
1368 | } |
1369 | } |
1370 | |
1371 | DSTZone tailZone = null; |
1372 | if (in.readBoolean()) { |
1373 | tailZone = DSTZone.readFrom(in, id); |
1374 | } |
1375 | |
1376 | return new PrecalculatedZone |
1377 | (id, transitions, wallOffsets, standardOffsets, nameKeys, tailZone); |
1378 | } |
1379 | |
1380 | /** |
1381 | * Factory to create instance from builder. |
1382 | * |
1383 | * @param id the zone id |
1384 | * @param outputID true if the zone id should be output |
1385 | * @param transitions the list of Transition objects |
1386 | * @param tailZone optional zone for getting info beyond precalculated tables |
1387 | */ |
1388 | static PrecalculatedZone create(String id, boolean outputID, ArrayList transitions, DSTZone tailZone) { |
1389 | int size = transitions.size(); |
1390 | if (size == 0) { |
1391 | throw new IllegalArgumentException(); |
1392 | } |
1393 | |
1394 | long[] trans = new long[size]; |
1395 | int[] wallOffsets = new int[size]; |
1396 | int[] standardOffsets = new int[size]; |
1397 | String[] nameKeys = new String[size]; |
1398 | |
1399 | Transition last = null; |
1400 | for (int i=0; i<size; i++) { |
1401 | Transition tr = (Transition)transitions.get(i); |
1402 | |
1403 | if (!tr.isTransitionFrom(last)) { |
1404 | throw new IllegalArgumentException(id); |
1405 | } |
1406 | |
1407 | trans[i] = tr.getMillis(); |
1408 | wallOffsets[i] = tr.getWallOffset(); |
1409 | standardOffsets[i] = tr.getStandardOffset(); |
1410 | nameKeys[i] = tr.getNameKey(); |
1411 | |
1412 | last = tr; |
1413 | } |
1414 | |
1415 | // Some timezones (Australia) have the same name key for |
1416 | // summer and winter which messes everything up. Fix it here. |
1417 | String[] zoneNameData = new String[5]; |
1418 | String[][] zoneStrings = new DateFormatSymbols(Locale.ENGLISH).getZoneStrings(); |
1419 | for (int j = 0; j < zoneStrings.length; j++) { |
1420 | String[] set = zoneStrings[j]; |
1421 | if (set != null && set.length == 5 && id.equals(set[0])) { |
1422 | zoneNameData = set; |
1423 | } |
1424 | } |
1425 | for (int i = 0; i < nameKeys.length - 1; i++) { |
1426 | String curNameKey = nameKeys[i]; |
1427 | String nextNameKey = nameKeys[i + 1]; |
1428 | long curOffset = wallOffsets[i]; |
1429 | long nextOffset = wallOffsets[i + 1]; |
1430 | long curStdOffset = standardOffsets[i]; |
1431 | long nextStdOffset = standardOffsets[i + 1]; |
1432 | Period p = new Period(trans[i], trans[i + 1], PeriodType.yearMonthDay()); |
1433 | if (curOffset != nextOffset && |
1434 | curStdOffset == nextStdOffset && |
1435 | curNameKey.equals(nextNameKey) && |
1436 | p.getYears() == 0 && p.getMonths() > 4 && p.getMonths() < 8 && |
1437 | curNameKey.equals(zoneNameData[2]) && |
1438 | curNameKey.equals(zoneNameData[4])) { |
1439 | |
1440 | System.out.println("Fixing duplicate name key - " + nextNameKey); |
1441 | System.out.println(" - " + new DateTime(trans[i]) + " - " + new DateTime(trans[i + 1])); |
1442 | if (curOffset > nextOffset) { |
1443 | nameKeys[i] = (curNameKey + "-Summer").intern(); |
1444 | } else if (curOffset < nextOffset) { |
1445 | nameKeys[i + 1] = (nextNameKey + "-Summer").intern(); |
1446 | i++; |
1447 | } |
1448 | } |
1449 | } |
1450 | if (tailZone != null) { |
1451 | if (tailZone.iStartRecurrence.getNameKey().equals(tailZone.iEndRecurrence.getNameKey())) { |
1452 | System.out.println("Fixing duplicate recurrent name key - " + tailZone.iStartRecurrence.getNameKey()); |
1453 | if (tailZone.iStartRecurrence.getSaveMillis() > 0) { |
1454 | tailZone = new DSTZone( |
1455 | tailZone.getID(), |
1456 | tailZone.iStandardOffset, |
1457 | tailZone.iStartRecurrence.renameAppend("-Summer"), |
1458 | tailZone.iEndRecurrence); |
1459 | } else { |
1460 | tailZone = new DSTZone( |
1461 | tailZone.getID(), |
1462 | tailZone.iStandardOffset, |
1463 | tailZone.iStartRecurrence, |
1464 | tailZone.iEndRecurrence.renameAppend("-Summer")); |
1465 | } |
1466 | } |
1467 | } |
1468 | |
1469 | return new PrecalculatedZone((outputID ? id : ""), trans, wallOffsets, standardOffsets, nameKeys, tailZone); |
1470 | } |
1471 | |
1472 | // All array fields have the same length. |
1473 | |
1474 | private final long[] iTransitions; |
1475 | |
1476 | private final int[] iWallOffsets; |
1477 | private final int[] iStandardOffsets; |
1478 | private final String[] iNameKeys; |
1479 | |
1480 | private final DSTZone iTailZone; |
1481 | |
1482 | /** |
1483 | * Constructor used ONLY for valid input, loaded via static methods. |
1484 | */ |
1485 | private PrecalculatedZone(String id, long[] transitions, int[] wallOffsets, |
1486 | int[] standardOffsets, String[] nameKeys, DSTZone tailZone) |
1487 | { |
1488 | super(id); |
1489 | iTransitions = transitions; |
1490 | iWallOffsets = wallOffsets; |
1491 | iStandardOffsets = standardOffsets; |
1492 | iNameKeys = nameKeys; |
1493 | iTailZone = tailZone; |
1494 | } |
1495 | |
1496 | public String getNameKey(long instant) { |
1497 | long[] transitions = iTransitions; |
1498 | int i = Arrays.binarySearch(transitions, instant); |
1499 | if (i >= 0) { |
1500 | return iNameKeys[i]; |
1501 | } |
1502 | i = ~i; |
1503 | if (i < transitions.length) { |
1504 | if (i > 0) { |
1505 | return iNameKeys[i - 1]; |
1506 | } |
1507 | return "UTC"; |
1508 | } |
1509 | if (iTailZone == null) { |
1510 | return iNameKeys[i - 1]; |
1511 | } |
1512 | return iTailZone.getNameKey(instant); |
1513 | } |
1514 | |
1515 | public int getOffset(long instant) { |
1516 | long[] transitions = iTransitions; |
1517 | int i = Arrays.binarySearch(transitions, instant); |
1518 | if (i >= 0) { |
1519 | return iWallOffsets[i]; |
1520 | } |
1521 | i = ~i; |
1522 | if (i < transitions.length) { |
1523 | if (i > 0) { |
1524 | return iWallOffsets[i - 1]; |
1525 | } |
1526 | return 0; |
1527 | } |
1528 | if (iTailZone == null) { |
1529 | return iWallOffsets[i - 1]; |
1530 | } |
1531 | return iTailZone.getOffset(instant); |
1532 | } |
1533 | |
1534 | public int getStandardOffset(long instant) { |
1535 | long[] transitions = iTransitions; |
1536 | int i = Arrays.binarySearch(transitions, instant); |
1537 | if (i >= 0) { |
1538 | return iStandardOffsets[i]; |
1539 | } |
1540 | i = ~i; |
1541 | if (i < transitions.length) { |
1542 | if (i > 0) { |
1543 | return iStandardOffsets[i - 1]; |
1544 | } |
1545 | return 0; |
1546 | } |
1547 | if (iTailZone == null) { |
1548 | return iStandardOffsets[i - 1]; |
1549 | } |
1550 | return iTailZone.getStandardOffset(instant); |
1551 | } |
1552 | |
1553 | public boolean isFixed() { |
1554 | return false; |
1555 | } |
1556 | |
1557 | public long nextTransition(long instant) { |
1558 | long[] transitions = iTransitions; |
1559 | int i = Arrays.binarySearch(transitions, instant); |
1560 | i = (i >= 0) ? (i + 1) : ~i; |
1561 | if (i < transitions.length) { |
1562 | return transitions[i]; |
1563 | } |
1564 | if (iTailZone == null) { |
1565 | return instant; |
1566 | } |
1567 | long end = transitions[transitions.length - 1]; |
1568 | if (instant < end) { |
1569 | instant = end; |
1570 | } |
1571 | return iTailZone.nextTransition(instant); |
1572 | } |
1573 | |
1574 | public long previousTransition(long instant) { |
1575 | long[] transitions = iTransitions; |
1576 | int i = Arrays.binarySearch(transitions, instant); |
1577 | if (i >= 0) { |
1578 | if (instant > Long.MIN_VALUE) { |
1579 | return instant - 1; |
1580 | } |
1581 | return instant; |
1582 | } |
1583 | i = ~i; |
1584 | if (i < transitions.length) { |
1585 | if (i > 0) { |
1586 | long prev = transitions[i - 1]; |
1587 | if (prev > Long.MIN_VALUE) { |
1588 | return prev - 1; |
1589 | } |
1590 | } |
1591 | return instant; |
1592 | } |
1593 | if (iTailZone != null) { |
1594 | long prev = iTailZone.previousTransition(instant); |
1595 | if (prev < instant) { |
1596 | return prev; |
1597 | } |
1598 | } |
1599 | long prev = transitions[i - 1]; |
1600 | if (prev > Long.MIN_VALUE) { |
1601 | return prev - 1; |
1602 | } |
1603 | return instant; |
1604 | } |
1605 | |
1606 | public boolean equals(Object obj) { |
1607 | if (this == obj) { |
1608 | return true; |
1609 | } |
1610 | if (obj instanceof PrecalculatedZone) { |
1611 | PrecalculatedZone other = (PrecalculatedZone)obj; |
1612 | return |
1613 | getID().equals(other.getID()) && |
1614 | Arrays.equals(iTransitions, other.iTransitions) && |
1615 | Arrays.equals(iNameKeys, other.iNameKeys) && |
1616 | Arrays.equals(iWallOffsets, other.iWallOffsets) && |
1617 | Arrays.equals(iStandardOffsets, other.iStandardOffsets) && |
1618 | ((iTailZone == null) |
1619 | ? (null == other.iTailZone) |
1620 | : (iTailZone.equals(other.iTailZone))); |
1621 | } |
1622 | return false; |
1623 | } |
1624 | |
1625 | public void writeTo(DataOutput out) throws IOException { |
1626 | int size = iTransitions.length; |
1627 | |
1628 | // Create unique string pool. |
1629 | Set poolSet = new HashSet(); |
1630 | for (int i=0; i<size; i++) { |
1631 | poolSet.add(iNameKeys[i]); |
1632 | } |
1633 | |
1634 | int poolSize = poolSet.size(); |
1635 | if (poolSize > 65535) { |
1636 | throw new UnsupportedOperationException("String pool is too large"); |
1637 | } |
1638 | String[] pool = new String[poolSize]; |
1639 | Iterator it = poolSet.iterator(); |
1640 | for (int i=0; it.hasNext(); i++) { |
1641 | pool[i] = (String)it.next(); |
1642 | } |
1643 | |
1644 | // Write out the pool. |
1645 | out.writeShort(poolSize); |
1646 | for (int i=0; i<poolSize; i++) { |
1647 | out.writeUTF(pool[i]); |
1648 | } |
1649 | |
1650 | out.writeInt(size); |
1651 | |
1652 | for (int i=0; i<size; i++) { |
1653 | writeMillis(out, iTransitions[i]); |
1654 | writeMillis(out, iWallOffsets[i]); |
1655 | writeMillis(out, iStandardOffsets[i]); |
1656 | |
1657 | // Find pool index and write it out. |
1658 | String nameKey = iNameKeys[i]; |
1659 | for (int j=0; j<poolSize; j++) { |
1660 | if (pool[j].equals(nameKey)) { |
1661 | if (poolSize < 256) { |
1662 | out.writeByte(j); |
1663 | } else { |
1664 | out.writeShort(j); |
1665 | } |
1666 | break; |
1667 | } |
1668 | } |
1669 | } |
1670 | |
1671 | out.writeBoolean(iTailZone != null); |
1672 | if (iTailZone != null) { |
1673 | iTailZone.writeTo(out); |
1674 | } |
1675 | } |
1676 | |
1677 | public boolean isCachable() { |
1678 | if (iTailZone != null) { |
1679 | return true; |
1680 | } |
1681 | long[] transitions = iTransitions; |
1682 | if (transitions.length <= 1) { |
1683 | return false; |
1684 | } |
1685 | |
1686 | // Add up all the distances between transitions that are less than |
1687 | // about two years. |
1688 | double distances = 0; |
1689 | int count = 0; |
1690 | |
1691 | for (int i=1; i<transitions.length; i++) { |
1692 | long diff = transitions[i] - transitions[i - 1]; |
1693 | if (diff < ((366L + 365) * 24 * 60 * 60 * 1000)) { |
1694 | distances += (double)diff; |
1695 | count++; |
1696 | } |
1697 | } |
1698 | |
1699 | if (count > 0) { |
1700 | double avg = distances / count; |
1701 | avg /= 24 * 60 * 60 * 1000; |
1702 | if (avg >= 25) { |
1703 | // Only bother caching if average distance between |
1704 | // transitions is at least 25 days. Why 25? |
1705 | // CachedDateTimeZone is more efficient if the distance |
1706 | // between transitions is large. With an average of 25, it |
1707 | // will on average perform about 2 tests per cache |
1708 | // hit. (49.7 / 25) is approximately 2. |
1709 | return true; |
1710 | } |
1711 | } |
1712 | |
1713 | return false; |
1714 | } |
1715 | } |
1716 | } |