1 /* 2 * Copyright (C) 2007 The Android Open Source Project 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 /* 17 * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the 18 * following header: 19 * 20 * This file is in the public domain, so clarified as of 21 * 1996-06-05 by Arthur David Olson. 22 */ 23 package libcore.util; 24 25 import java.io.IOException; 26 import java.io.ObjectInputStream; 27 import java.util.Arrays; 28 import java.util.Calendar; 29 import java.util.Date; 30 import java.util.GregorianCalendar; 31 import java.util.TimeZone; 32 import libcore.io.BufferIterator; 33 34 /** 35 * Our concrete TimeZone implementation, backed by zoneinfo data. 36 * 37 * <p>This reads time zone information from a binary file stored on the platform. The binary file 38 * is essentially a single file containing compacted versions of all the tzfile (see 39 * {@code man 5 tzfile} for details of the source) and an index by long name, e.g. Europe/London. 40 * 41 * <p>The compacted form is created by {@code external/icu/tools/ZoneCompactor.java} and is used 42 * by both this and Bionic. {@link ZoneInfoDB} is responsible for mapping the binary file, and 43 * reading the index and creating a {@link BufferIterator} that provides access to an entry for a 44 * specific file. This class is responsible for reading the data from that {@link BufferIterator} 45 * and storing it a representation to support the {@link TimeZone} and {@link GregorianCalendar} 46 * implementations. See {@link ZoneInfo#readTimeZone(String, BufferIterator, long)}. 47 * 48 * <p>The main difference between {@code tzfile} and the compacted form is that the 49 * {@code struct ttinfo} only uses a single byte for {@code tt_isdst} and {@code tt_abbrind}. 50 * 51 * <p>This class does not use all the information from the {@code tzfile}; it uses: 52 * {@code tzh_timecnt} and the associated transition times and type information. For each type 53 * (described by {@code struct ttinfo}) it uses {@code tt_gmtoff} and {@code tt_isdst}. Note, that 54 * the definition of {@code struct ttinfo} uses {@code long}, and {@code int} but they do not have 55 * the same meaning as Java. The prose following the definition makes it clear that the {@code long} 56 * is 4 bytes and the {@code int} fields are 1 byte. 57 * 58 * <p>As the data uses 32 bits to store the time in seconds the time range is limited to roughly 59 * 69 years either side of the epoch (1st Jan 1970 00:00:00) that means that it cannot handle any 60 * dates before 1900 and after 2038. There is an extended version of the table that uses 64 bits 61 * to store the data but that information is not used by this. 62 * 63 * @hide - used to implement TimeZone 64 */ 65 public final class ZoneInfo extends TimeZone { 66 private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; 67 private static final long MILLISECONDS_PER_400_YEARS = 68 MILLISECONDS_PER_DAY * (400 * 365 + 100 - 3); 69 70 private static final long UNIX_OFFSET = 62167219200000L; 71 72 private static final int[] NORMAL = new int[] { 73 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 74 }; 75 76 private static final int[] LEAP = new int[] { 77 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 78 }; 79 80 // Proclaim serialization compatibility with pre-OpenJDK AOSP 81 static final long serialVersionUID = -4598738130123921552L; 82 83 private int mRawOffset; 84 private final int mEarliestRawOffset; 85 86 /** 87 * Implements {@link #useDaylightTime()} 88 * 89 * <p>True if the transition active at the time this instance was created, or future 90 * transitions support DST. It is possible that caching this value at construction time and 91 * using it for the lifetime of the instance does not match the contract of the 92 * {@link TimeZone#useDaylightTime()} method but it appears to be what the RI does and that 93 * method is not particularly useful when it comes to historical or future times as it does not 94 * allow the time to be specified. 95 * 96 * <p>When this is false then {@link #mDstSavings} will be 0. 97 * 98 * @see #mDstSavings 99 */ 100 private final boolean mUseDst; 101 102 /** 103 * Implements {@link #getDSTSavings()} 104 * 105 * <p>This should be final but is not because it may need to be fixed up by 106 * {@link #readObject(ObjectInputStream)} to correct an inconsistency in the previous version 107 * of the code whereby this was set to a non-zero value even though DST was not actually used. 108 * 109 * @see #mUseDst 110 */ 111 private int mDstSavings; 112 113 /** 114 * The times (in seconds) at which the offsets changes for any reason, whether that is a change 115 * in the offset from UTC or a change in the DST. 116 * 117 * <p>These times are pre-calculated externally from a set of rules (both historical and 118 * future) and stored in a file from which {@link ZoneInfo#readTimeZone(String, BufferIterator, 119 * long)} reads the data. That is quite different to {@link java.util.SimpleTimeZone}, which has 120 * essentially human readable rules (e.g. DST starts at 01:00 on the first Sunday in March and 121 * ends at 01:00 on the last Sunday in October) that can be used to determine the DST transition 122 * times across a number of years 123 * 124 * <p>In terms of {@link ZoneInfo tzfile} structure this array is of length {@code tzh_timecnt} 125 * and contains the times in seconds converted to long to make them safer to use. 126 * 127 * <p>They are stored in order from earliest (lowest) time to latest (highest). A transition is 128 * identified by its index within this array. A transition {@code T} is active at a specific 129 * time {@code X} if {@code T} is the highest transition whose time is less than or equal to 130 * {@code X}. 131 * 132 * @see #mTypes 133 */ 134 private final long[] mTransitions; 135 136 /** 137 * The type of the transition, where type is a pair consisting of the offset and whether the 138 * offset includes DST or not. 139 * 140 * <p>Each transition in {@link #mTransitions} has an associated type in this array at the same 141 * index. The type is an index into the arrays {@link #mOffsets} and {@link #mIsDsts} that each 142 * contain one part of the pair. 143 * 144 * <p>In the {@link ZoneInfo tzfile} structure the type array only contains unique instances of 145 * the {@code struct ttinfo} to save space and each type may be referenced by multiple 146 * transitions. However, the type pairs stored in this class are not guaranteed unique because 147 * they do not include the {@code tt_abbrind}, which is the abbreviated identifier to use for 148 * the time zone after the transition. 149 * 150 * @see #mTransitions 151 * @see #mOffsets 152 * @see #mIsDsts 153 */ 154 private final byte[] mTypes; 155 156 /** 157 * The offset parts of the transition types, in seconds. 158 * 159 * <p>These are actually a delta to the {@link #mRawOffset}. So, if the offset is say +7200 160 * seconds and {@link #mRawOffset} is say +3600 then this will have a value of +3600. 161 * 162 * <p>The offset in milliseconds can be computed using: 163 * {@code mRawOffset + mOffsets[type] * 1000} 164 * 165 * @see #mTypes 166 * @see #mIsDsts 167 */ 168 private final int[] mOffsets; 169 170 /** 171 * Specifies whether an associated offset includes DST or not. 172 * 173 * <p>Each entry in here is 1 if the offset at the same index in {@link #mOffsets} includes DST 174 * and 0 otherwise. 175 * 176 * @see #mTypes 177 * @see #mOffsets 178 */ 179 private final byte[] mIsDsts; 180 181 public static ZoneInfo readTimeZone(String id, BufferIterator it, long currentTimeMillis) 182 throws IOException { 183 // Variable names beginning tzh_ correspond to those in "tzfile.h". 184 185 // Check tzh_magic. 186 int tzh_magic = it.readInt(); 187 if (tzh_magic != 0x545a6966) { // "TZif" 188 throw new IOException("Timezone id=" + id + " has an invalid header=" + tzh_magic); 189 } 190 191 // Skip the uninteresting part of the header. 192 it.skip(28); 193 194 // Read the sizes of the arrays we're about to read. 195 int tzh_timecnt = it.readInt(); 196 // Arbitrary ceiling to prevent allocating memory for corrupt data. 197 // 2 per year with 2^32 seconds would give ~272 transitions. 198 final int MAX_TRANSITIONS = 2000; 199 if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) { 200 throw new IOException( 201 "Timezone id=" + id + " has an invalid number of transitions=" + tzh_timecnt); 202 } 203 204 int tzh_typecnt = it.readInt(); 205 final int MAX_TYPES = 256; 206 if (tzh_typecnt < 1) { 207 throw new IOException("ZoneInfo requires at least one type " 208 + "to be provided for each timezone but could not find one for '" + id + "'"); 209 } else if (tzh_typecnt > MAX_TYPES) { 210 throw new IOException( 211 "Timezone with id " + id + " has too many types=" + tzh_typecnt); 212 } 213 214 it.skip(4); // Skip tzh_charcnt. 215 216 // Transitions are signed 32 bit integers, but we store them as signed 64 bit 217 // integers since it's easier to compare them against 64 bit inputs (see getOffset 218 // and isDaylightTime) with much less risk of an overflow in our calculations. 219 // 220 // The alternative of checking the input against the first and last transition in 221 // the array is far more awkward and error prone. 222 int[] transitions32 = new int[tzh_timecnt]; 223 it.readIntArray(transitions32, 0, transitions32.length); 224 225 long[] transitions64 = new long[tzh_timecnt]; 226 for (int i = 0; i < tzh_timecnt; ++i) { 227 transitions64[i] = transitions32[i]; 228 if (i > 0 && transitions64[i] <= transitions64[i - 1]) { 229 throw new IOException( 230 id + " transition at " + i + " is not sorted correctly, is " 231 + transitions64[i] + ", previous is " + transitions64[i - 1]); 232 } 233 } 234 235 byte[] type = new byte[tzh_timecnt]; 236 it.readByteArray(type, 0, type.length); 237 for (int i = 0; i < type.length; i++) { 238 int typeIndex = type[i] & 0xff; 239 if (typeIndex >= tzh_typecnt) { 240 throw new IOException( 241 id + " type at " + i + " is not < " + tzh_typecnt + ", is " + typeIndex); 242 } 243 } 244 245 int[] gmtOffsets = new int[tzh_typecnt]; 246 byte[] isDsts = new byte[tzh_typecnt]; 247 for (int i = 0; i < tzh_typecnt; ++i) { 248 gmtOffsets[i] = it.readInt(); 249 byte isDst = it.readByte(); 250 if (isDst != 0 && isDst != 1) { 251 throw new IOException(id + " dst at " + i + " is not 0 or 1, is " + isDst); 252 } 253 isDsts[i] = isDst; 254 // We skip the abbreviation index. This would let us provide historically-accurate 255 // time zone abbreviations (such as "AHST", "YST", and "AKST" for standard time in 256 // America/Anchorage in 1982, 1983, and 1984 respectively). ICU only knows the current 257 // names, though, so even if we did use this data to provide the correct abbreviations 258 // for en_US, we wouldn't be able to provide correct abbreviations for other locales, 259 // nor would we be able to provide correct long forms (such as "Yukon Standard Time") 260 // for any locale. (The RI doesn't do any better than us here either.) 261 it.skip(1); 262 } 263 264 return new ZoneInfo(id, transitions64, type, gmtOffsets, isDsts, currentTimeMillis); 265 } 266 267 private ZoneInfo(String name, long[] transitions, byte[] types, int[] gmtOffsets, byte[] isDsts, 268 long currentTimeMillis) { 269 if (gmtOffsets.length == 0) { 270 throw new IllegalArgumentException("ZoneInfo requires at least one offset " 271 + "to be provided for each timezone but could not find one for '" + name + "'"); 272 } 273 mTransitions = transitions; 274 mTypes = types; 275 mIsDsts = isDsts; 276 setID(name); 277 278 // Find the latest daylight and standard offsets (if any). 279 int lastStd = -1; 280 int lastDst = -1; 281 for (int i = mTransitions.length - 1; (lastStd == -1 || lastDst == -1) && i >= 0; --i) { 282 int type = mTypes[i] & 0xff; 283 if (lastStd == -1 && mIsDsts[type] == 0) { 284 lastStd = i; 285 } 286 if (lastDst == -1 && mIsDsts[type] != 0) { 287 lastDst = i; 288 } 289 } 290 291 // Use the latest non-daylight offset (if any) as the raw offset. 292 if (mTransitions.length == 0) { 293 // If there are no transitions then use the first GMT offset. 294 mRawOffset = gmtOffsets[0]; 295 } else { 296 if (lastStd == -1) { 297 throw new IllegalStateException( "ZoneInfo requires at least one non-DST " 298 + "transition to be provided for each timezone that has at least one " 299 + "transition but could not find one for '" + name + "'"); 300 } 301 mRawOffset = gmtOffsets[mTypes[lastStd] & 0xff]; 302 } 303 304 if (lastDst != -1) { 305 // Check to see if the last DST transition is in the future or the past. If it is in 306 // the past then we treat it as if it doesn't exist, at least for the purposes of 307 // setting mDstSavings and mUseDst. 308 long lastDSTTransitionTime = mTransitions[lastDst]; 309 310 // Convert the current time in millis into seconds. Unlike other places that convert 311 // time in milliseconds into seconds in order to compare with transition time this 312 // rounds up rather than down. It does that because this is interested in what 313 // transitions apply in future 314 long currentUnixTimeSeconds = roundUpMillisToSeconds(currentTimeMillis); 315 316 // Is this zone observing DST currently or in the future? 317 // We don't care if they've historically used it: most places have at least once. 318 // See http://code.google.com/p/android/issues/detail?id=877. 319 // This test means that for somewhere like Morocco, which tried DST in 2009 but has 320 // no future plans (and thus no future schedule info) will report "true" from 321 // useDaylightTime at the start of 2009 but "false" at the end. This seems appropriate. 322 if (lastDSTTransitionTime < currentUnixTimeSeconds) { 323 // The last DST transition is before now so treat it as if it doesn't exist. 324 lastDst = -1; 325 } 326 } 327 328 if (lastDst == -1) { 329 // There were no DST transitions or at least no future DST transitions so DST is not 330 // used. 331 mDstSavings = 0; 332 mUseDst = false; 333 } else { 334 // Use the latest transition's pair of offsets to compute the DST savings. 335 // This isn't generally useful, but it's exposed by TimeZone.getDSTSavings. 336 int lastGmtOffset = gmtOffsets[mTypes[lastStd] & 0xff]; 337 int lastDstOffset = gmtOffsets[mTypes[lastDst] & 0xff]; 338 mDstSavings = Math.abs(lastGmtOffset - lastDstOffset) * 1000; 339 mUseDst = true; 340 } 341 342 // Cache the oldest known raw offset, in case we're asked about times that predate our 343 // transition data. 344 int firstStd = -1; 345 for (int i = 0; i < mTransitions.length; ++i) { 346 if (mIsDsts[mTypes[i] & 0xff] == 0) { 347 firstStd = i; 348 break; 349 } 350 } 351 int earliestRawOffset = (firstStd != -1) ? gmtOffsets[mTypes[firstStd] & 0xff] : mRawOffset; 352 353 // Rather than keep offsets from UTC, we use offsets from local time, so the raw offset 354 // can be changed and automatically affect all the offsets. 355 mOffsets = gmtOffsets; 356 for (int i = 0; i < mOffsets.length; i++) { 357 mOffsets[i] -= mRawOffset; 358 } 359 360 // tzdata uses seconds, but Java uses milliseconds. 361 mRawOffset *= 1000; 362 mEarliestRawOffset = earliestRawOffset * 1000; 363 } 364 365 /** 366 * Ensure that when deserializing an instance that {@link #mDstSavings} is always 0 when 367 * {@link #mUseDst} is false. 368 */ 369 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 370 in.defaultReadObject(); 371 if (!mUseDst && mDstSavings != 0) { 372 mDstSavings = 0; 373 } 374 } 375 376 @Override 377 public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) { 378 // XXX This assumes Gregorian always; Calendar switches from 379 // Julian to Gregorian in 1582. What calendar system are the 380 // arguments supposed to come from? 381 382 long calc = (year / 400) * MILLISECONDS_PER_400_YEARS; 383 year %= 400; 384 385 calc += year * (365 * MILLISECONDS_PER_DAY); 386 calc += ((year + 3) / 4) * MILLISECONDS_PER_DAY; 387 388 if (year > 0) { 389 calc -= ((year - 1) / 100) * MILLISECONDS_PER_DAY; 390 } 391 392 boolean isLeap = (year == 0 || (year % 4 == 0 && year % 100 != 0)); 393 int[] mlen = isLeap ? LEAP : NORMAL; 394 395 calc += mlen[month] * MILLISECONDS_PER_DAY; 396 calc += (day - 1) * MILLISECONDS_PER_DAY; 397 calc += millis; 398 399 calc -= mRawOffset; 400 calc -= UNIX_OFFSET; 401 402 return getOffset(calc); 403 } 404 405 /** 406 * Find the transition in the {@code timezone} in effect at {@code seconds}. 407 * 408 * <p>Returns an index in the range -1..timeZone.mTransitions.length - 1. -1 is used to 409 * indicate the time is before the first transition. Other values are an index into 410 * timeZone.mTransitions. 411 */ 412 public int findTransitionIndex(long seconds) { 413 int transition = Arrays.binarySearch(mTransitions, seconds); 414 if (transition < 0) { 415 transition = ~transition - 1; 416 if (transition < 0) { 417 return -1; 418 } 419 } 420 421 return transition; 422 } 423 424 /** 425 * Finds the index within the {@link #mOffsets}/{@link #mIsDsts} arrays for the specified time 426 * in seconds, since 1st Jan 1970 00:00:00. 427 * @param seconds the time in seconds. 428 * @return -1 if the time is before the first transition, or [0..{@code mOffsets}-1] for the 429 * active offset. 430 */ 431 int findOffsetIndexForTimeInSeconds(long seconds) { 432 int transition = findTransitionIndex(seconds); 433 if (transition < 0) { 434 return -1; 435 } 436 437 return mTypes[transition] & 0xff; 438 } 439 440 /** 441 * Finds the index within the {@link #mOffsets}/{@link #mIsDsts} arrays for the specified time 442 * in milliseconds, since 1st Jan 1970 00:00:00.000. 443 * @param millis the time in milliseconds. 444 * @return -1 if the time is before the first transition, or [0..{@code mOffsets}-1] for the 445 * active offset. 446 */ 447 int findOffsetIndexForTimeInMilliseconds(long millis) { 448 // This rounds the time in milliseconds down to the time in seconds. 449 // 450 // It can't just divide a timestamp in millis by 1000 to obtain a transition time in 451 // seconds because / (div) in Java rounds towards zero. Times before 1970 are negative and 452 // if they have a millisecond component then div would result in obtaining a time that is 453 // one second after what we need. 454 // 455 // e.g. dividing -12,001 milliseconds by 1000 would result in -12 seconds. If there was a 456 // transition at -12 seconds then that would be incorrectly treated as being active 457 // for a time of -12,001 milliseconds even though that time is before the transition 458 // should occur. 459 460 return findOffsetIndexForTimeInSeconds(roundDownMillisToSeconds(millis)); 461 } 462 463 /** 464 * Converts time in milliseconds into a time in seconds, rounding down to the closest time 465 * in seconds before the time in milliseconds. 466 * 467 * <p>It's not sufficient to simply divide by 1000 because that rounds towards 0 and so while 468 * for positive numbers it produces a time in seconds that precedes the time in milliseconds 469 * for negative numbers it can produce a time in seconds that follows the time in milliseconds. 470 * 471 * <p>This basically does the same as {@code (long) Math.floor(millis / 1000.0)} but should be 472 * faster. 473 * 474 * @param millis the time in milliseconds, may be negative. 475 * @return the time in seconds. 476 */ 477 static long roundDownMillisToSeconds(long millis) { 478 if (millis < 0) { 479 // If the time is less than zero then subtract 999 and then divide by 1000 rounding 480 // towards 0 as usual, e.g. 481 // -12345 -> -13344 / 1000 = -13 482 // -12000 -> -12999 / 1000 = -12 483 // -12001 -> -13000 / 1000 = -13 484 return (millis - 999) / 1000; 485 } else { 486 return millis / 1000; 487 } 488 } 489 490 /** 491 * Converts time in milliseconds into a time in seconds, rounding up to the closest time 492 * in seconds before the time in milliseconds. 493 * 494 * <p>It's not sufficient to simply divide by 1000 because that rounds towards 0 and so while 495 * for negative numbers it produces a time in seconds that follows the time in milliseconds 496 * for positive numbers it can produce a time in seconds that precedes the time in milliseconds. 497 * 498 * <p>This basically does the same as {@code (long) Math.ceil(millis / 1000.0)} but should be 499 * faster. 500 * 501 * @param millis the time in milliseconds, may be negative. 502 * @return the time in seconds. 503 */ 504 static long roundUpMillisToSeconds(long millis) { 505 if (millis > 0) { 506 // If the time is greater than zero then add 999 and then divide by 1000 rounding 507 // towards 0 as usual, e.g. 508 // 12345 -> 13344 / 1000 = 13 509 // 12000 -> 12999 / 1000 = 12 510 // 12001 -> 13000 / 1000 = 13 511 return (millis + 999) / 1000; 512 } else { 513 return millis / 1000; 514 } 515 } 516 517 /** 518 * Get the raw and DST offsets for the specified time in milliseconds since 519 * 1st Jan 1970 00:00:00.000 UTC. 520 * 521 * <p>The raw offset, i.e. that part of the total offset which is not due to DST, is stored at 522 * index 0 of the {@code offsets} array and the DST offset, i.e. that part of the offset which 523 * is due to DST is stored at index 1. 524 * 525 * @param utcTimeInMillis the UTC time in milliseconds. 526 * @param offsets the array whose length must be greater than or equal to 2. 527 * @return the total offset which is the sum of the raw and DST offsets. 528 */ 529 public int getOffsetsByUtcTime(long utcTimeInMillis, int[] offsets) { 530 int transitionIndex = findTransitionIndex(roundDownMillisToSeconds(utcTimeInMillis)); 531 int totalOffset; 532 int rawOffset; 533 int dstOffset; 534 if (transitionIndex == -1) { 535 // See getOffset(long) and inDaylightTime(Date) for an explanation as to why these 536 // values are used for times before the first transition. 537 rawOffset = mEarliestRawOffset; 538 dstOffset = 0; 539 totalOffset = rawOffset; 540 } else { 541 int type = mTypes[transitionIndex] & 0xff; 542 543 // Get the total offset used for the transition. 544 totalOffset = mRawOffset + mOffsets[type] * 1000; 545 if (mIsDsts[type] == 0) { 546 // Offset does not include DST so DST is 0 and the raw offset is the total offset. 547 rawOffset = totalOffset; 548 dstOffset = 0; 549 } else { 550 // Offset does include DST, we need to find the preceding transition that did not 551 // include the DST offset so that we can calculate the DST offset. 552 rawOffset = -1; 553 for (transitionIndex -= 1; transitionIndex >= 0; --transitionIndex) { 554 type = mTypes[transitionIndex] & 0xff; 555 if (mIsDsts[type] == 0) { 556 rawOffset = mRawOffset + mOffsets[type] * 1000; 557 break; 558 } 559 } 560 // If no previous transition was found then use the earliest raw offset. 561 if (rawOffset == -1) { 562 rawOffset = mEarliestRawOffset; 563 } 564 565 // The DST offset is the difference between the total and the raw offset. 566 dstOffset = totalOffset - rawOffset; 567 } 568 } 569 570 offsets[0] = rawOffset; 571 offsets[1] = dstOffset; 572 573 return totalOffset; 574 } 575 576 @Override 577 public int getOffset(long when) { 578 int offsetIndex = findOffsetIndexForTimeInMilliseconds(when); 579 if (offsetIndex == -1) { 580 // Assume that all times before our first transition correspond to the 581 // oldest-known non-daylight offset. The obvious alternative would be to 582 // use the current raw offset, but that seems like a greater leap of faith. 583 return mEarliestRawOffset; 584 } 585 return mRawOffset + mOffsets[offsetIndex] * 1000; 586 } 587 588 @Override public boolean inDaylightTime(Date time) { 589 long when = time.getTime(); 590 int offsetIndex = findOffsetIndexForTimeInMilliseconds(when); 591 if (offsetIndex == -1) { 592 // Assume that all times before our first transition are non-daylight. 593 // Transition data tends to start with a transition to daylight, so just 594 // copying the first transition would assume the opposite. 595 // http://code.google.com/p/android/issues/detail?id=14395 596 return false; 597 } 598 return mIsDsts[offsetIndex] == 1; 599 } 600 601 @Override public int getRawOffset() { 602 return mRawOffset; 603 } 604 605 @Override public void setRawOffset(int off) { 606 mRawOffset = off; 607 } 608 609 @Override public int getDSTSavings() { 610 return mDstSavings; 611 } 612 613 @Override public boolean useDaylightTime() { 614 return mUseDst; 615 } 616 617 @Override public boolean hasSameRules(TimeZone timeZone) { 618 if (!(timeZone instanceof ZoneInfo)) { 619 return false; 620 } 621 ZoneInfo other = (ZoneInfo) timeZone; 622 if (mUseDst != other.mUseDst) { 623 return false; 624 } 625 if (!mUseDst) { 626 return mRawOffset == other.mRawOffset; 627 } 628 return mRawOffset == other.mRawOffset 629 // Arrays.equals returns true if both arrays are null 630 && Arrays.equals(mOffsets, other.mOffsets) 631 && Arrays.equals(mIsDsts, other.mIsDsts) 632 && Arrays.equals(mTypes, other.mTypes) 633 && Arrays.equals(mTransitions, other.mTransitions); 634 } 635 636 @Override public boolean equals(Object obj) { 637 if (!(obj instanceof ZoneInfo)) { 638 return false; 639 } 640 ZoneInfo other = (ZoneInfo) obj; 641 return getID().equals(other.getID()) && hasSameRules(other); 642 } 643 644 @Override 645 public int hashCode() { 646 final int prime = 31; 647 int result = 1; 648 result = prime * result + getID().hashCode(); 649 result = prime * result + Arrays.hashCode(mOffsets); 650 result = prime * result + Arrays.hashCode(mIsDsts); 651 result = prime * result + mRawOffset; 652 result = prime * result + Arrays.hashCode(mTransitions); 653 result = prime * result + Arrays.hashCode(mTypes); 654 result = prime * result + (mUseDst ? 1231 : 1237); 655 return result; 656 } 657 658 @Override 659 public String toString() { 660 return getClass().getName() + "[id=\"" + getID() + "\"" + 661 ",mRawOffset=" + mRawOffset + 662 ",mEarliestRawOffset=" + mEarliestRawOffset + 663 ",mUseDst=" + mUseDst + 664 ",mDstSavings=" + mDstSavings + 665 ",transitions=" + mTransitions.length + 666 "]"; 667 } 668 669 @Override 670 public Object clone() { 671 // Overridden for documentation. The default clone() behavior is exactly what we want. 672 // Though mutable, the arrays of offset data are treated as immutable. Only ID and 673 // mRawOffset are mutable in this class, and those are an immutable object and a primitive 674 // respectively. 675 return super.clone(); 676 } 677 678 /** 679 * A class that represents a "wall time". This class is modeled on the C tm struct and 680 * is used to support android.text.format.Time behavior. Unlike the tm struct the year is 681 * represented as the full year, not the years since 1900. 682 * 683 * <p>This class contains a rewrite of various native functions that android.text.format.Time 684 * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap 685 * seconds but does try to preserve behavior around ambiguous date/times found in the BSD 686 * version of mktime that was previously used. 687 * 688 * <p>The original native code used a 32-bit value for time_t on 32-bit Android, which 689 * was the only variant of Android available at the time. To preserve old behavior this code 690 * deliberately uses {@code int} rather than {@code long} for most things and performs 691 * calculations in seconds. This creates deliberate truncation issues for date / times before 692 * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons 693 * can be resolved: Application code may have come to rely on the range so previously values 694 * like zero for year could indicate an invalid date but if we move to long the year zero would 695 * be valid. 696 * 697 * <p>All offsets are considered to be safe for addition / subtraction / multiplication without 698 * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow. 699 */ 700 public static class WallTime { 701 702 // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic 703 // and to convert from a broken-down date/time to a millis value. 704 // Unfortunately, it cannot represent an initial state with a zero day and would 705 // automatically normalize it, so we must copy values into and out of it as needed. 706 private final GregorianCalendar calendar; 707 708 private int year; 709 private int month; 710 private int monthDay; 711 private int hour; 712 private int minute; 713 private int second; 714 private int weekDay; 715 private int yearDay; 716 private int isDst; 717 private int gmtOffsetSeconds; 718 719 public WallTime() { 720 this.calendar = new GregorianCalendar(0, 0, 0, 0, 0, 0); 721 calendar.setTimeZone(TimeZone.getTimeZone("UTC")); 722 } 723 724 /** 725 * Sets the wall time to a point in time using the time zone information provided. This 726 * is a replacement for the old native localtime_tz() function. 727 * 728 * <p>When going from an instant to a wall time it is always unambiguous because there 729 * is only one offset rule acting at any given instant. We do not consider leap seconds. 730 */ 731 public void localtime(int timeSeconds, ZoneInfo zoneInfo) { 732 try { 733 int offsetSeconds = zoneInfo.mRawOffset / 1000; 734 735 // Find out the timezone DST state and adjustment. 736 byte isDst; 737 if (zoneInfo.mTransitions.length == 0) { 738 isDst = 0; 739 } else { 740 // offsetIndex can be in the range -1..zoneInfo.mOffsets.length - 1 741 int offsetIndex = zoneInfo.findOffsetIndexForTimeInSeconds(timeSeconds); 742 if (offsetIndex == -1) { 743 // -1 means timeSeconds is "before the first recorded transition". The first 744 // recorded transition is treated as a transition from non-DST and the raw 745 // offset. 746 isDst = 0; 747 } else { 748 offsetSeconds += zoneInfo.mOffsets[offsetIndex]; 749 isDst = zoneInfo.mIsDsts[offsetIndex]; 750 } 751 } 752 753 // Perform arithmetic that might underflow before setting fields. 754 int wallTimeSeconds = checkedAdd(timeSeconds, offsetSeconds); 755 756 // Set fields. 757 calendar.setTimeInMillis(wallTimeSeconds * 1000L); 758 copyFieldsFromCalendar(); 759 this.isDst = isDst; 760 this.gmtOffsetSeconds = offsetSeconds; 761 } catch (CheckedArithmeticException e) { 762 // Just stop, leaving fields untouched. 763 } 764 } 765 766 /** 767 * Returns the time in seconds since beginning of the Unix epoch for the wall time using the 768 * time zone information provided. This is a replacement for an old native mktime_tz() C 769 * function. 770 * 771 * <p>When going from a wall time to an instant the answer can be ambiguous. A wall 772 * time can map to zero, one or two instants given sane date/time transitions. Sane 773 * in this case means that transitions occur less frequently than the offset 774 * differences between them (which could cause all sorts of craziness like the 775 * skipping out of transitions). 776 * 777 * <p>For example, this is not fully supported: 778 * <ul> 779 * <li>t1 { time = 1, offset = 0 } 780 * <li>t2 { time = 2, offset = -1 } 781 * <li>t3 { time = 3, offset = -2 } 782 * </ul> 783 * A wall time in this case might map to t1, t2 or t3. 784 * 785 * <p>We do not handle leap seconds. 786 * <p>We assume that no timezone offset transition has an absolute offset > 24 hours. 787 * <p>We do not assume that adjacent transitions modify the DST state; adjustments can 788 * occur for other reasons such as when a zone changes its raw offset. 789 */ 790 public int mktime(ZoneInfo zoneInfo) { 791 // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below. 792 this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0; 793 794 copyFieldsToCalendar(); 795 final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000; 796 if (Integer.MIN_VALUE > longWallTimeSeconds 797 || longWallTimeSeconds > Integer.MAX_VALUE) { 798 // For compatibility with the old native 32-bit implementation we must treat 799 // this as an error. Note: -1 could be confused with a real time. 800 return -1; 801 } 802 803 try { 804 final int wallTimeSeconds = (int) longWallTimeSeconds; 805 final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; 806 final int rawTimeSeconds = checkedSubtract(wallTimeSeconds, rawOffsetSeconds); 807 808 if (zoneInfo.mTransitions.length == 0) { 809 // There is no transition information. There is just a raw offset for all time. 810 if (this.isDst > 0) { 811 // Caller has asserted DST, but there is no DST information available. 812 return -1; 813 } 814 copyFieldsFromCalendar(); 815 this.isDst = 0; 816 this.gmtOffsetSeconds = rawOffsetSeconds; 817 return rawTimeSeconds; 818 } 819 820 // We cannot know for sure what instant the wall time will map to. Unfortunately, in 821 // order to know for sure we need the timezone information, but to get the timezone 822 // information we need an instant. To resolve this we use the raw offset to find an 823 // OffsetInterval; this will get us the OffsetInterval we need or very close. 824 825 // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1 826 // indicates the rawTime is before the first transition and is handled gracefully by 827 // createOffsetInterval(). 828 final int initialTransitionIndex = zoneInfo.findTransitionIndex(rawTimeSeconds); 829 830 if (isDst < 0) { 831 // This is treated as a special case to get it out of the way: 832 // When a caller has set isDst == -1 it means we can return the first match for 833 // the wall time we find. If the caller has specified a wall time that cannot 834 // exist this always returns -1. 835 836 Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, 837 wallTimeSeconds, true /* mustMatchDst */); 838 return result == null ? -1 : result; 839 } 840 841 // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice: 842 // 1) The first attempts to find a DST offset that matches isDst exactly. 843 // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see 844 // if a valid wall time can be created. The result can be somewhat arbitrary. 845 846 Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, 847 true /* mustMatchDst */); 848 if (result == null) { 849 result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, 850 false /* mustMatchDst */); 851 } 852 if (result == null) { 853 result = -1; 854 } 855 return result; 856 } catch (CheckedArithmeticException e) { 857 return -1; 858 } 859 } 860 861 /** 862 * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in 863 * {@code targetInterval}. 864 * 865 * <p>This is used when a caller has made an assertion about standard time / DST that cannot 866 * be matched to any offset interval that exists. We must therefore assume that the isDst 867 * assertion is incorrect and the invalid wall time is the result of some modification the 868 * caller made to a valid wall time that pushed them outside of the offset interval they 869 * were in. We must correct for any DST change that should have been applied when they did 870 * so. 871 * 872 * <p>Unfortunately, we have no information about what adjustment they made and so cannot 873 * know which offset interval they were previously in. For example, they may have added a 874 * second or a year to a valid time to arrive at what they have. 875 * 876 * <p>We try all offset types that are not the same as the isDst the caller asserted. For 877 * each possible offset we work out the offset difference between that and 878 * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If 879 * we are, then we have found an adjustment. 880 */ 881 private Integer tryOffsetAdjustments(ZoneInfo zoneInfo, int oldWallTimeSeconds, 882 OffsetInterval targetInterval, int transitionIndex, int isDstToFind) 883 throws CheckedArithmeticException { 884 885 int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind); 886 for (int j = 0; j < offsetsToTry.length; j++) { 887 int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; 888 int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j]; 889 int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds(); 890 int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds; 891 int adjustedWallTimeSeconds = checkedAdd(oldWallTimeSeconds, adjustmentSeconds); 892 if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) { 893 // Perform any arithmetic that might overflow. 894 int returnValue = checkedSubtract(adjustedWallTimeSeconds, 895 targetIntervalOffsetSeconds); 896 897 // Modify field state and return the result. 898 calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L); 899 copyFieldsFromCalendar(); 900 this.isDst = targetInterval.getIsDst(); 901 this.gmtOffsetSeconds = targetIntervalOffsetSeconds; 902 return returnValue; 903 } 904 } 905 return null; 906 } 907 908 /** 909 * Return an array of offsets that have the requested {@code isDst} value. 910 * The {@code startIndex} is used as a starting point so transitions nearest 911 * to that index are returned first. 912 */ 913 private static int[] getOffsetsOfType(ZoneInfo zoneInfo, int startIndex, int isDst) { 914 // +1 to account for the synthetic transition we invent before the first recorded one. 915 int[] offsets = new int[zoneInfo.mOffsets.length + 1]; 916 boolean[] seen = new boolean[zoneInfo.mOffsets.length]; 917 int numFound = 0; 918 919 int delta = 0; 920 boolean clampTop = false; 921 boolean clampBottom = false; 922 do { 923 // delta = { 1, -1, 2, -2, 3, -3...} 924 delta *= -1; 925 if (delta >= 0) { 926 delta++; 927 } 928 929 int transitionIndex = startIndex + delta; 930 if (delta < 0 && transitionIndex < -1) { 931 clampBottom = true; 932 continue; 933 } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) { 934 clampTop = true; 935 continue; 936 } 937 938 if (transitionIndex == -1) { 939 if (isDst == 0) { 940 // Synthesize a non-DST transition before the first transition we have 941 // data for. 942 offsets[numFound++] = 0; // offset of 0 from raw offset 943 } 944 continue; 945 } 946 int type = zoneInfo.mTypes[transitionIndex] & 0xff; 947 if (!seen[type]) { 948 if (zoneInfo.mIsDsts[type] == isDst) { 949 offsets[numFound++] = zoneInfo.mOffsets[type]; 950 } 951 seen[type] = true; 952 } 953 } while (!(clampTop && clampBottom)); 954 955 int[] toReturn = new int[numFound]; 956 System.arraycopy(offsets, 0, toReturn, 0, numFound); 957 return toReturn; 958 } 959 960 /** 961 * Find a time <em>in seconds</em> the same or close to {@code wallTimeSeconds} that 962 * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition 963 * with {@code initialTransitionIndex}. 964 * 965 * <p>If {@code mustMatchDst} is {@code true} the method can only return times that 966 * use timezone offsets that satisfy the {@code this.isDst} requirements. 967 * If {@code this.isDst == -1} it means that any offset can be used. 968 * 969 * <p>If {@code mustMatchDst} is {@code false} any offset that covers the 970 * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset 971 * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}. 972 * 973 * <p>Note: This method both uses and can modify field state. It returns the matching time 974 * in seconds if a match has been found and modifies fields, or it returns {@code null} and 975 * leaves the field state unmodified. 976 */ 977 private Integer doWallTimeSearch(ZoneInfo zoneInfo, int initialTransitionIndex, 978 int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException { 979 980 // The loop below starts at the initialTransitionIndex and radiates out from that point 981 // up to 24 hours in either direction by applying transitionIndexDelta to inspect 982 // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no 983 // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to 984 // indicate whether the search has either searched > 24 hours or exhausted the 985 // transition data in that direction. The search stops when a match is found or if 986 // clampTop and clampBottom are both true. 987 // The match logic employed is determined by the mustMatchDst parameter. 988 final int MAX_SEARCH_SECONDS = 24 * 60 * 60; 989 boolean clampTop = false, clampBottom = false; 990 int loop = 0; 991 do { 992 // transitionIndexDelta = { 0, -1, 1, -2, 2,..} 993 int transitionIndexDelta = (loop + 1) / 2; 994 if (loop % 2 == 1) { 995 transitionIndexDelta *= -1; 996 } 997 loop++; 998 999 // Only do any work in this iteration if we need to. 1000 if (transitionIndexDelta > 0 && clampTop 1001 || transitionIndexDelta < 0 && clampBottom) { 1002 continue; 1003 } 1004 1005 // Obtain the OffsetInterval to use. 1006 int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta; 1007 OffsetInterval offsetInterval = 1008 OffsetInterval.create(zoneInfo, currentTransitionIndex); 1009 if (offsetInterval == null) { 1010 // No transition exists with the index we tried: Stop searching in the 1011 // current direction. 1012 clampTop |= (transitionIndexDelta > 0); 1013 clampBottom |= (transitionIndexDelta < 0); 1014 continue; 1015 } 1016 1017 // Match the wallTimeSeconds against the OffsetInterval. 1018 if (mustMatchDst) { 1019 // Work out if the interval contains the wall time the caller specified and 1020 // matches their isDst value. 1021 if (offsetInterval.containsWallTime(wallTimeSeconds)) { 1022 if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) { 1023 // This always returns the first OffsetInterval it finds that matches 1024 // the wall time and isDst requirements. If this.isDst == -1 this means 1025 // the result might be a DST or a non-DST answer for wall times that can 1026 // exist in two OffsetIntervals. 1027 int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds(); 1028 int returnValue = checkedSubtract(wallTimeSeconds, 1029 totalOffsetSeconds); 1030 1031 copyFieldsFromCalendar(); 1032 this.isDst = offsetInterval.getIsDst(); 1033 this.gmtOffsetSeconds = totalOffsetSeconds; 1034 return returnValue; 1035 } 1036 } 1037 } else { 1038 // To retain similar behavior to the old native implementation: if the caller is 1039 // asserting the same isDst value as the OffsetInterval we are looking at we do 1040 // not try to find an adjustment from another OffsetInterval of the same isDst 1041 // type. If you remove this you get different results in situations like a 1042 // DST -> DST transition or STD -> STD transition that results in an interval of 1043 // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two 1044 // DST intervals, and the caller has passed isDst == 1, this results in a -1 1045 // being returned. 1046 if (isDst != offsetInterval.getIsDst()) { 1047 final int isDstToFind = isDst; 1048 Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds, 1049 offsetInterval, currentTransitionIndex, isDstToFind); 1050 if (returnValue != null) { 1051 return returnValue; 1052 } 1053 } 1054 } 1055 1056 // See if we can avoid another loop in the current direction. 1057 if (transitionIndexDelta > 0) { 1058 // If we are searching forward and the OffsetInterval we have ends 1059 // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further 1060 // forward. 1061 boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds 1062 > MAX_SEARCH_SECONDS; 1063 if (endSearch) { 1064 clampTop = true; 1065 } 1066 } else if (transitionIndexDelta < 0) { 1067 boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds() 1068 >= MAX_SEARCH_SECONDS; 1069 if (endSearch) { 1070 // If we are searching backward and the OffsetInterval starts 1071 // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any 1072 // further backwards. 1073 clampBottom = true; 1074 } 1075 } 1076 } while (!(clampTop && clampBottom)); 1077 return null; 1078 } 1079 1080 public void setYear(int year) { 1081 this.year = year; 1082 } 1083 1084 public void setMonth(int month) { 1085 this.month = month; 1086 } 1087 1088 public void setMonthDay(int monthDay) { 1089 this.monthDay = monthDay; 1090 } 1091 1092 public void setHour(int hour) { 1093 this.hour = hour; 1094 } 1095 1096 public void setMinute(int minute) { 1097 this.minute = minute; 1098 } 1099 1100 public void setSecond(int second) { 1101 this.second = second; 1102 } 1103 1104 public void setWeekDay(int weekDay) { 1105 this.weekDay = weekDay; 1106 } 1107 1108 public void setYearDay(int yearDay) { 1109 this.yearDay = yearDay; 1110 } 1111 1112 public void setIsDst(int isDst) { 1113 this.isDst = isDst; 1114 } 1115 1116 public void setGmtOffset(int gmtoff) { 1117 this.gmtOffsetSeconds = gmtoff; 1118 } 1119 1120 public int getYear() { 1121 return year; 1122 } 1123 1124 public int getMonth() { 1125 return month; 1126 } 1127 1128 public int getMonthDay() { 1129 return monthDay; 1130 } 1131 1132 public int getHour() { 1133 return hour; 1134 } 1135 1136 public int getMinute() { 1137 return minute; 1138 } 1139 1140 public int getSecond() { 1141 return second; 1142 } 1143 1144 public int getWeekDay() { 1145 return weekDay; 1146 } 1147 1148 public int getYearDay() { 1149 return yearDay; 1150 } 1151 1152 public int getGmtOffset() { 1153 return gmtOffsetSeconds; 1154 } 1155 1156 public int getIsDst() { 1157 return isDst; 1158 } 1159 1160 private void copyFieldsToCalendar() { 1161 calendar.set(Calendar.YEAR, year); 1162 calendar.set(Calendar.MONTH, month); 1163 calendar.set(Calendar.DAY_OF_MONTH, monthDay); 1164 calendar.set(Calendar.HOUR_OF_DAY, hour); 1165 calendar.set(Calendar.MINUTE, minute); 1166 calendar.set(Calendar.SECOND, second); 1167 calendar.set(Calendar.MILLISECOND, 0); 1168 } 1169 1170 private void copyFieldsFromCalendar() { 1171 year = calendar.get(Calendar.YEAR); 1172 month = calendar.get(Calendar.MONTH); 1173 monthDay = calendar.get(Calendar.DAY_OF_MONTH); 1174 hour = calendar.get(Calendar.HOUR_OF_DAY); 1175 minute = calendar.get(Calendar.MINUTE); 1176 second = calendar.get(Calendar.SECOND); 1177 1178 // Calendar uses Sunday == 1. Android Time uses Sunday = 0. 1179 weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1; 1180 // Calendar enumerates from 1, Android Time enumerates from 0. 1181 yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1; 1182 } 1183 } 1184 1185 /** 1186 * A wall-time representation of a timezone offset interval. 1187 * 1188 * <p>Wall-time means "as it would appear locally in the timezone in which it applies". 1189 * For example in 2007: 1190 * PST was a -8:00 offset that ran until Mar 11, 2:00 AM. 1191 * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM. 1192 * PST was a -8:00 offset and ran from Nov 4, 1:00 AM. 1193 * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when 1194 * PDT ended and PST began. 1195 * 1196 * <p>For convenience all wall-time values are represented as the number of seconds since the 1197 * beginning of the Unix epoch <em>in UTC</em>. To convert from a wall-time to the actual time 1198 * in the offset it is necessary to <em>subtract</em> the {@code totalOffsetSeconds}. 1199 * For example: If the offset in PST is -07:00 hours, then: 1200 * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds 1201 * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST 1202 */ 1203 static class OffsetInterval { 1204 1205 private final int startWallTimeSeconds; 1206 private final int endWallTimeSeconds; 1207 private final int isDst; 1208 private final int totalOffsetSeconds; 1209 1210 /** 1211 * Creates an {@link OffsetInterval}. 1212 * 1213 * <p>If {@code transitionIndex} is -1, the transition is synthesized to be a non-DST offset 1214 * that runs from the beginning of time until the first transition in {@code timeZone} and 1215 * has an offset of {@code timezone.mRawOffset}. If {@code transitionIndex} is the last 1216 * transition that transition is considered to run until the end of representable time. 1217 * Otherwise, the information is extracted from {@code timeZone.mTransitions}, 1218 * {@code timeZone.mOffsets} an {@code timeZone.mIsDsts}. 1219 */ 1220 public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex) 1221 throws CheckedArithmeticException { 1222 1223 if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) { 1224 return null; 1225 } 1226 1227 int rawOffsetSeconds = timeZone.mRawOffset / 1000; 1228 if (transitionIndex == -1) { 1229 int endWallTimeSeconds = checkedAdd(timeZone.mTransitions[0], rawOffsetSeconds); 1230 return new OffsetInterval(Integer.MIN_VALUE, endWallTimeSeconds, 0 /* isDst */, 1231 rawOffsetSeconds); 1232 } 1233 1234 int type = timeZone.mTypes[transitionIndex] & 0xff; 1235 int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds; 1236 int endWallTimeSeconds; 1237 if (transitionIndex == timeZone.mTransitions.length - 1) { 1238 // If this is the last transition, make up the end time. 1239 endWallTimeSeconds = Integer.MAX_VALUE; 1240 } else { 1241 endWallTimeSeconds = checkedAdd(timeZone.mTransitions[transitionIndex + 1], 1242 totalOffsetSeconds); 1243 } 1244 int isDst = timeZone.mIsDsts[type]; 1245 int startWallTimeSeconds = 1246 checkedAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds); 1247 return new OffsetInterval( 1248 startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); 1249 } 1250 1251 private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst, 1252 int totalOffsetSeconds) { 1253 this.startWallTimeSeconds = startWallTimeSeconds; 1254 this.endWallTimeSeconds = endWallTimeSeconds; 1255 this.isDst = isDst; 1256 this.totalOffsetSeconds = totalOffsetSeconds; 1257 } 1258 1259 public boolean containsWallTime(long wallTimeSeconds) { 1260 return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds; 1261 } 1262 1263 public int getIsDst() { 1264 return isDst; 1265 } 1266 1267 public int getTotalOffsetSeconds() { 1268 return totalOffsetSeconds; 1269 } 1270 1271 public long getEndWallTimeSeconds() { 1272 return endWallTimeSeconds; 1273 } 1274 1275 public long getStartWallTimeSeconds() { 1276 return startWallTimeSeconds; 1277 } 1278 } 1279 1280 /** 1281 * An exception used to indicate an arithmetic overflow or underflow. 1282 */ 1283 private static class CheckedArithmeticException extends Exception { 1284 } 1285 1286 /** 1287 * Calculate (a + b). 1288 * 1289 * @throws CheckedArithmeticException if overflow or underflow occurs 1290 */ 1291 private static int checkedAdd(long a, int b) throws CheckedArithmeticException { 1292 // Adapted from Guava IntMath.checkedAdd(); 1293 long result = a + b; 1294 if (result != (int) result) { 1295 throw new CheckedArithmeticException(); 1296 } 1297 return (int) result; 1298 } 1299 1300 /** 1301 * Calculate (a - b). 1302 * 1303 * @throws CheckedArithmeticException if overflow or underflow occurs 1304 */ 1305 private static int checkedSubtract(int a, int b) throws CheckedArithmeticException { 1306 // Adapted from Guava IntMath.checkedSubtract(); 1307 long result = (long) a - b; 1308 if (result != (int) result) { 1309 throw new CheckedArithmeticException(); 1310 } 1311 return (int) result; 1312 } 1313 } 1314