ZoneInfo.java revision b7d5826277766003e36bb8e82a0c09020fc1c823
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 */ 23package libcore.util; 24 25import java.util.Arrays; 26import java.util.Calendar; 27import java.util.Date; 28import java.util.GregorianCalendar; 29import java.util.TimeZone; 30import libcore.io.BufferIterator; 31 32/** 33 * Our concrete TimeZone implementation, backed by zoneinfo data. 34 * 35 * @hide - used to implement TimeZone 36 */ 37public final class ZoneInfo extends TimeZone { 38 private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; 39 private static final long MILLISECONDS_PER_400_YEARS = 40 MILLISECONDS_PER_DAY * (400 * 365 + 100 - 3); 41 42 private static final long UNIX_OFFSET = 62167219200000L; 43 44 private static final int[] NORMAL = new int[] { 45 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 46 }; 47 48 private static final int[] LEAP = new int[] { 49 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 50 }; 51 52 private int mRawOffset; 53 private final int mEarliestRawOffset; 54 private final boolean mUseDst; 55 private final int mDstSavings; // Implements TimeZone.getDSTSavings. 56 57 private final int[] mTransitions; 58 private final int[] mOffsets; 59 private final byte[] mTypes; 60 private final byte[] mIsDsts; 61 62 public static ZoneInfo makeTimeZone(String id, BufferIterator it) { 63 // Variable names beginning tzh_ correspond to those in "tzfile.h". 64 65 // Check tzh_magic. 66 if (it.readInt() != 0x545a6966) { // "TZif" 67 return null; 68 } 69 70 // Skip the uninteresting part of the header. 71 it.skip(28); 72 73 // Read the sizes of the arrays we're about to read. 74 int tzh_timecnt = it.readInt(); 75 int tzh_typecnt = it.readInt(); 76 77 it.skip(4); // Skip tzh_charcnt. 78 79 int[] transitions = new int[tzh_timecnt]; 80 it.readIntArray(transitions, 0, transitions.length); 81 82 byte[] type = new byte[tzh_timecnt]; 83 it.readByteArray(type, 0, type.length); 84 85 int[] gmtOffsets = new int[tzh_typecnt]; 86 byte[] isDsts = new byte[tzh_typecnt]; 87 for (int i = 0; i < tzh_typecnt; ++i) { 88 gmtOffsets[i] = it.readInt(); 89 isDsts[i] = it.readByte(); 90 // We skip the abbreviation index. This would let us provide historically-accurate 91 // time zone abbreviations (such as "AHST", "YST", and "AKST" for standard time in 92 // America/Anchorage in 1982, 1983, and 1984 respectively). ICU only knows the current 93 // names, though, so even if we did use this data to provide the correct abbreviations 94 // for en_US, we wouldn't be able to provide correct abbreviations for other locales, 95 // nor would we be able to provide correct long forms (such as "Yukon Standard Time") 96 // for any locale. (The RI doesn't do any better than us here either.) 97 it.skip(1); 98 } 99 100 return new ZoneInfo(id, transitions, type, gmtOffsets, isDsts); 101 } 102 103 private ZoneInfo(String name, int[] transitions, byte[] types, int[] gmtOffsets, byte[] isDsts) { 104 mTransitions = transitions; 105 mTypes = types; 106 mIsDsts = isDsts; 107 setID(name); 108 109 // Find the latest daylight and standard offsets (if any). 110 int lastStd = 0; 111 boolean haveStd = false; 112 int lastDst = 0; 113 boolean haveDst = false; 114 for (int i = mTransitions.length - 1; (!haveStd || !haveDst) && i >= 0; --i) { 115 int type = mTypes[i] & 0xff; 116 if (!haveStd && mIsDsts[type] == 0) { 117 haveStd = true; 118 lastStd = i; 119 } 120 if (!haveDst && mIsDsts[type] != 0) { 121 haveDst = true; 122 lastDst = i; 123 } 124 } 125 126 // Use the latest non-daylight offset (if any) as the raw offset. 127 if (lastStd >= mTypes.length) { 128 mRawOffset = gmtOffsets[0]; 129 } else { 130 mRawOffset = gmtOffsets[mTypes[lastStd] & 0xff]; 131 } 132 133 // Use the latest transition's pair of offsets to compute the DST savings. 134 // This isn't generally useful, but it's exposed by TimeZone.getDSTSavings. 135 if (lastDst >= mTypes.length) { 136 mDstSavings = 0; 137 } else { 138 mDstSavings = Math.abs(gmtOffsets[mTypes[lastStd] & 0xff] - gmtOffsets[mTypes[lastDst] & 0xff]) * 1000; 139 } 140 141 // Cache the oldest known raw offset, in case we're asked about times that predate our 142 // transition data. 143 int firstStd = -1; 144 for (int i = 0; i < mTransitions.length; ++i) { 145 if (mIsDsts[mTypes[i] & 0xff] == 0) { 146 firstStd = i; 147 break; 148 } 149 } 150 int earliestRawOffset = (firstStd != -1) ? gmtOffsets[mTypes[firstStd] & 0xff] : mRawOffset; 151 152 // Rather than keep offsets from UTC, we use offsets from local time, so the raw offset 153 // can be changed and automatically affect all the offsets. 154 mOffsets = gmtOffsets; 155 for (int i = 0; i < mOffsets.length; i++) { 156 mOffsets[i] -= mRawOffset; 157 } 158 159 // Is this zone still observing DST? 160 // We don't care if they've historically used it: most places have at least once. 161 // We want to know whether the last "schedule info" (the unix times in the mTransitions 162 // array) is in the future. If it is, DST is still relevant. 163 // See http://code.google.com/p/android/issues/detail?id=877. 164 // This test means that for somewhere like Morocco, which tried DST in 2009 but has 165 // no future plans (and thus no future schedule info) will report "true" from 166 // useDaylightTime at the start of 2009 but "false" at the end. This seems appropriate. 167 boolean usesDst = false; 168 long currentUnixTime = System.currentTimeMillis() / 1000; 169 if (mTransitions.length > 0) { 170 // (We're really dealing with uint32_t values, so long is most convenient in Java.) 171 long latestScheduleTime = ((long) mTransitions[mTransitions.length - 1]) & 0xffffffff; 172 if (currentUnixTime < latestScheduleTime) { 173 usesDst = true; 174 } 175 } 176 mUseDst = usesDst; 177 178 // tzdata uses seconds, but Java uses milliseconds. 179 mRawOffset *= 1000; 180 mEarliestRawOffset = earliestRawOffset * 1000; 181 } 182 183 @Override 184 public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) { 185 // XXX This assumes Gregorian always; Calendar switches from 186 // Julian to Gregorian in 1582. What calendar system are the 187 // arguments supposed to come from? 188 189 long calc = (year / 400) * MILLISECONDS_PER_400_YEARS; 190 year %= 400; 191 192 calc += year * (365 * MILLISECONDS_PER_DAY); 193 calc += ((year + 3) / 4) * MILLISECONDS_PER_DAY; 194 195 if (year > 0) { 196 calc -= ((year - 1) / 100) * MILLISECONDS_PER_DAY; 197 } 198 199 boolean isLeap = (year == 0 || (year % 4 == 0 && year % 100 != 0)); 200 int[] mlen = isLeap ? LEAP : NORMAL; 201 202 calc += mlen[month] * MILLISECONDS_PER_DAY; 203 calc += (day - 1) * MILLISECONDS_PER_DAY; 204 calc += millis; 205 206 calc -= mRawOffset; 207 calc -= UNIX_OFFSET; 208 209 return getOffset(calc); 210 } 211 212 @Override 213 public int getOffset(long when) { 214 int unix = (int) (when / 1000); 215 int transition = Arrays.binarySearch(mTransitions, unix); 216 if (transition < 0) { 217 transition = ~transition - 1; 218 if (transition < 0) { 219 // Assume that all times before our first transition correspond to the 220 // oldest-known non-daylight offset. The obvious alternative would be to 221 // use the current raw offset, but that seems like a greater leap of faith. 222 return mEarliestRawOffset; 223 } 224 } 225 return mRawOffset + mOffsets[mTypes[transition] & 0xff] * 1000; 226 } 227 228 @Override public boolean inDaylightTime(Date time) { 229 long when = time.getTime(); 230 int unix = (int) (when / 1000); 231 int transition = Arrays.binarySearch(mTransitions, unix); 232 if (transition < 0) { 233 transition = ~transition - 1; 234 if (transition < 0) { 235 // Assume that all times before our first transition are non-daylight. 236 // Transition data tends to start with a transition to daylight, so just 237 // copying the first transition would assume the opposite. 238 // http://code.google.com/p/android/issues/detail?id=14395 239 return false; 240 } 241 } 242 return mIsDsts[mTypes[transition] & 0xff] == 1; 243 } 244 245 @Override public int getRawOffset() { 246 return mRawOffset; 247 } 248 249 @Override public void setRawOffset(int off) { 250 mRawOffset = off; 251 } 252 253 @Override public int getDSTSavings() { 254 return mUseDst ? mDstSavings: 0; 255 } 256 257 @Override public boolean useDaylightTime() { 258 return mUseDst; 259 } 260 261 @Override public boolean hasSameRules(TimeZone timeZone) { 262 if (!(timeZone instanceof ZoneInfo)) { 263 return false; 264 } 265 ZoneInfo other = (ZoneInfo) timeZone; 266 if (mUseDst != other.mUseDst) { 267 return false; 268 } 269 if (!mUseDst) { 270 return mRawOffset == other.mRawOffset; 271 } 272 return mRawOffset == other.mRawOffset 273 // Arrays.equals returns true if both arrays are null 274 && Arrays.equals(mOffsets, other.mOffsets) 275 && Arrays.equals(mIsDsts, other.mIsDsts) 276 && Arrays.equals(mTypes, other.mTypes) 277 && Arrays.equals(mTransitions, other.mTransitions); 278 } 279 280 @Override public boolean equals(Object obj) { 281 if (!(obj instanceof ZoneInfo)) { 282 return false; 283 } 284 ZoneInfo other = (ZoneInfo) obj; 285 return getID().equals(other.getID()) && hasSameRules(other); 286 } 287 288 @Override 289 public int hashCode() { 290 final int prime = 31; 291 int result = 1; 292 result = prime * result + getID().hashCode(); 293 result = prime * result + Arrays.hashCode(mOffsets); 294 result = prime * result + Arrays.hashCode(mIsDsts); 295 result = prime * result + mRawOffset; 296 result = prime * result + Arrays.hashCode(mTransitions); 297 result = prime * result + Arrays.hashCode(mTypes); 298 result = prime * result + (mUseDst ? 1231 : 1237); 299 return result; 300 } 301 302 @Override 303 public String toString() { 304 return getClass().getName() + "[id=\"" + getID() + "\"" + 305 ",mRawOffset=" + mRawOffset + 306 ",mEarliestRawOffset=" + mEarliestRawOffset + 307 ",mUseDst=" + mUseDst + 308 ",mDstSavings=" + mDstSavings + 309 ",transitions=" + mTransitions.length + 310 "]"; 311 } 312 313 @Override 314 public Object clone() { 315 // Overridden for documentation. The default clone() behavior is exactly what we want. 316 // Though mutable, the arrays of offset data are treated as immutable. Only ID and 317 // mRawOffset are mutable in this class, and those are an immutable object and a primitive 318 // respectively. 319 return super.clone(); 320 } 321 322 /** 323 * A class that represents a "wall time". This class is modeled on the C tm struct and 324 * is used to support android.text.format.Time behavior. Unlike the tm struct the year is 325 * represented as the full year, not the years since 1900. 326 * 327 * <p>This class contains a rewrite of various native functions that android.text.format.Time 328 * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap 329 * seconds but does try to preserve behavior around ambiguous date/times found in the BSD 330 * version of mktime that was previously used. 331 * 332 * <p>The original native code used a 32-bit value for time_t on 32-bit Android, which 333 * was the only variant of Android available at the time. To preserve old behavior this code 334 * deliberately uses {@code int} rather than {@code long} for most things and performs 335 * calculations in seconds. This creates deliberate truncation issues for date / times before 336 * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons 337 * can be resolved: Application code may have come to rely on the range so previously values 338 * like zero for year could indicate an invalid date but if we move to long the year zero would 339 * be valid. 340 * 341 * <p>All offsets are considered to be safe for addition / subtraction / multiplication without 342 * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow. 343 */ 344 public static class WallTime { 345 346 // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic 347 // and to convert from a broken-down date/time to a millis value. 348 // Unfortunately, it cannot represent an initial state with a zero day and would 349 // automatically normalize it, so we must copy values into and out of it as needed. 350 private final GregorianCalendar calendar; 351 352 private int year; 353 private int month; 354 private int monthDay; 355 private int hour; 356 private int minute; 357 private int second; 358 private int weekDay; 359 private int yearDay; 360 private int isDst; 361 private int gmtOffsetSeconds; 362 363 public WallTime() { 364 this.calendar = new GregorianCalendar(false); 365 calendar.setTimeZone(TimeZone.getTimeZone("UTC")); 366 } 367 368 /** 369 * Sets the wall time to a point in time using the time zone information provided. This 370 * is a replacement for the old native localtime_tz() function. 371 * 372 * <p>When going from an instant to a wall time it is always unambiguous because there 373 * is only one offset rule acting at any given instant. We do not consider leap seconds. 374 */ 375 public void localtime(int timeSeconds, ZoneInfo zoneInfo) { 376 try { 377 int offsetSeconds = zoneInfo.mRawOffset / 1000; 378 379 // Find out the timezone DST state and adjustment. 380 byte isDst; 381 if (zoneInfo.mTransitions.length == 0) { 382 isDst = 0; 383 } else { 384 // transitionIndex can be in the range -1..zoneInfo.mTransitions.length - 1 385 int transitionIndex = findTransitionIndex(zoneInfo, timeSeconds); 386 if (transitionIndex < 0) { 387 // -1 means timeSeconds is "before the first recorded transition". The first 388 // recorded transition is treated as a transition from non-DST and the raw 389 // offset. 390 isDst = 0; 391 } else { 392 byte transitionType = zoneInfo.mTypes[transitionIndex]; 393 offsetSeconds += zoneInfo.mOffsets[transitionType]; 394 isDst = zoneInfo.mIsDsts[transitionType]; 395 } 396 } 397 398 // Perform arithmetic that might underflow before setting fields. 399 int wallTimeSeconds = checkedAdd(timeSeconds, offsetSeconds); 400 401 // Set fields. 402 calendar.setTimeInMillis(wallTimeSeconds * 1000L); 403 copyFieldsFromCalendar(); 404 this.isDst = isDst; 405 this.gmtOffsetSeconds = offsetSeconds; 406 } catch (CheckedArithmeticException e) { 407 // Just stop, leaving fields untouched. 408 } 409 } 410 411 /** 412 * Returns the time in seconds since beginning of the Unix epoch for the wall time using the 413 * time zone information provided. This is a replacement for an old native mktime_tz() C 414 * function. 415 * 416 * <p>When going from a wall time to an instant the answer can be ambiguous. A wall 417 * time can map to zero, one or two instants given sane date/time transitions. Sane 418 * in this case means that transitions occur less frequently than the offset 419 * differences between them (which could cause all sorts of craziness like the 420 * skipping out of transitions). 421 * 422 * <p>For example, this is not fully supported: 423 * <ul> 424 * <li>t1 { time = 1, offset = 0 } 425 * <li>t2 { time = 2, offset = -1 } 426 * <li>t3 { time = 3, offset = -2 } 427 * </ul> 428 * A wall time in this case might map to t1, t2 or t3. 429 * 430 * <p>We do not handle leap seconds. 431 * <p>We assume that no timezone offset transition has an absolute offset > 24 hours. 432 * <p>We do not assume that adjacent transitions modify the DST state; adjustments can 433 * occur for other reasons such as when a zone changes its raw offset. 434 */ 435 public int mktime(ZoneInfo zoneInfo) { 436 // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below. 437 this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0; 438 439 copyFieldsToCalendar(); 440 final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000; 441 if (Integer.MIN_VALUE > longWallTimeSeconds 442 || longWallTimeSeconds > Integer.MAX_VALUE) { 443 // For compatibility with the old native 32-bit implementation we must treat 444 // this as an error. Note: -1 could be confused with a real time. 445 return -1; 446 } 447 448 try { 449 final int wallTimeSeconds = (int) longWallTimeSeconds; 450 final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; 451 final int rawTimeSeconds = checkedSubtract(wallTimeSeconds, rawOffsetSeconds); 452 453 if (zoneInfo.mTransitions.length == 0) { 454 // There is no transition information. There is just a raw offset for all time. 455 if (this.isDst > 0) { 456 // Caller has asserted DST, but there is no DST information available. 457 return -1; 458 } 459 copyFieldsFromCalendar(); 460 this.isDst = 0; 461 this.gmtOffsetSeconds = rawOffsetSeconds; 462 return rawTimeSeconds; 463 } 464 465 // We cannot know for sure what instant the wall time will map to. Unfortunately, in 466 // order to know for sure we need the timezone information, but to get the timezone 467 // information we need an instant. To resolve this we use the raw offset to find an 468 // OffsetInterval; this will get us the OffsetInterval we need or very close. 469 470 // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1 471 // indicates the rawTime is before the first transition and is handled gracefully by 472 // createOffsetInterval(). 473 final int initialTransitionIndex = findTransitionIndex(zoneInfo, rawTimeSeconds); 474 475 if (isDst < 0) { 476 // This is treated as a special case to get it out of the way: 477 // When a caller has set isDst == -1 it means we can return the first match for 478 // the wall time we find. If the caller has specified a wall time that cannot 479 // exist this always returns -1. 480 481 Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, 482 wallTimeSeconds, true /* mustMatchDst */); 483 return result == null ? -1 : result; 484 } 485 486 // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice: 487 // 1) The first attempts to find a DST offset that matches isDst exactly. 488 // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see 489 // if a valid wall time can be created. The result can be somewhat arbitrary. 490 491 Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, 492 true /* mustMatchDst */); 493 if (result == null) { 494 result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, 495 false /* mustMatchDst */); 496 } 497 if (result == null) { 498 result = -1; 499 } 500 return result; 501 } catch (CheckedArithmeticException e) { 502 return -1; 503 } 504 } 505 506 /** 507 * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in 508 * {@code targetInterval}. 509 * 510 * <p>This is used when a caller has made an assertion about standard time / DST that cannot 511 * be matched to any offset interval that exists. We must therefore assume that the isDst 512 * assertion is incorrect and the invalid wall time is the result of some modification the 513 * caller made to a valid wall time that pushed them outside of the offset interval they 514 * were in. We must correct for any DST change that should have been applied when they did 515 * so. 516 * 517 * <p>Unfortunately, we have no information about what adjustment they made and so cannot 518 * know which offset interval they were previously in. For example, they may have added a 519 * second or a year to a valid time to arrive at what they have. 520 * 521 * <p>We try all offset types that are not the same as the isDst the caller asserted. For 522 * each possible offset we work out the offset difference between that and 523 * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If 524 * we are, then we have found an adjustment. 525 */ 526 private Integer tryOffsetAdjustments(ZoneInfo zoneInfo, int oldWallTimeSeconds, 527 OffsetInterval targetInterval, int transitionIndex, int isDstToFind) 528 throws CheckedArithmeticException { 529 530 int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind); 531 for (int j = 0; j < offsetsToTry.length; j++) { 532 int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; 533 int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j]; 534 int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds(); 535 int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds; 536 int adjustedWallTimeSeconds = checkedAdd(oldWallTimeSeconds, adjustmentSeconds); 537 if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) { 538 // Perform any arithmetic that might overflow. 539 int returnValue = checkedSubtract(adjustedWallTimeSeconds, 540 targetIntervalOffsetSeconds); 541 542 // Modify field state and return the result. 543 calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L); 544 copyFieldsFromCalendar(); 545 this.isDst = targetInterval.getIsDst(); 546 this.gmtOffsetSeconds = targetIntervalOffsetSeconds; 547 return returnValue; 548 } 549 } 550 return null; 551 } 552 553 /** 554 * Return an array of offsets that have the requested {@code isDst} value. 555 * The {@code startIndex} is used as a starting point so transitions nearest 556 * to that index are returned first. 557 */ 558 private static int[] getOffsetsOfType(ZoneInfo zoneInfo, int startIndex, int isDst) { 559 // +1 to account for the synthetic transition we invent before the first recorded one. 560 int[] offsets = new int[zoneInfo.mOffsets.length + 1]; 561 boolean[] seen = new boolean[zoneInfo.mOffsets.length]; 562 int numFound = 0; 563 564 int delta = 0; 565 boolean clampTop = false; 566 boolean clampBottom = false; 567 do { 568 // delta = { 1, -1, 2, -2, 3, -3...} 569 delta *= -1; 570 if (delta >= 0) { 571 delta++; 572 } 573 574 int transitionIndex = startIndex + delta; 575 if (delta < 0 && transitionIndex < -1) { 576 clampBottom = true; 577 continue; 578 } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) { 579 clampTop = true; 580 continue; 581 } 582 583 if (transitionIndex == -1) { 584 if (isDst == 0) { 585 // Synthesize a non-DST transition before the first transition we have 586 // data for. 587 offsets[numFound++] = 0; // offset of 0 from raw offset 588 } 589 continue; 590 } 591 byte type = zoneInfo.mTypes[transitionIndex]; 592 if (!seen[type]) { 593 if (zoneInfo.mIsDsts[type] == isDst) { 594 offsets[numFound++] = zoneInfo.mOffsets[type]; 595 } 596 seen[type] = true; 597 } 598 } while (!(clampTop && clampBottom)); 599 600 int[] toReturn = new int[numFound]; 601 System.arraycopy(offsets, 0, toReturn, 0, numFound); 602 return toReturn; 603 } 604 605 /** 606 * Find a time <em>in seconds</em> the same or close to {@code wallTimeSeconds} that 607 * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition 608 * with {@code initialTransitionIndex}. 609 * 610 * <p>If {@code mustMatchDst} is {@code true} the method can only return times that 611 * use timezone offsets that satisfy the {@code this.isDst} requirements. 612 * If {@code this.isDst == -1} it means that any offset can be used. 613 * 614 * <p>If {@code mustMatchDst} is {@code false} any offset that covers the 615 * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset 616 * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}. 617 * 618 * <p>Note: This method both uses and can modify field state. It returns the matching time 619 * in seconds if a match has been found and modifies fields, or it returns {@code null} and 620 * leaves the field state unmodified. 621 */ 622 private Integer doWallTimeSearch(ZoneInfo zoneInfo, int initialTransitionIndex, 623 int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException { 624 625 // The loop below starts at the initialTransitionIndex and radiates out from that point 626 // up to 24 hours in either direction by applying transitionIndexDelta to inspect 627 // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no 628 // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to 629 // indicate whether the search has either searched > 24 hours or exhausted the 630 // transition data in that direction. The search stops when a match is found or if 631 // clampTop and clampBottom are both true. 632 // The match logic employed is determined by the mustMatchDst parameter. 633 final int MAX_SEARCH_SECONDS = 24 * 60 * 60; 634 boolean clampTop = false, clampBottom = false; 635 int loop = 0; 636 do { 637 // transitionIndexDelta = { 0, -1, 1, -2, 2,..} 638 int transitionIndexDelta = (loop + 1) / 2; 639 if (loop % 2 == 1) { 640 transitionIndexDelta *= -1; 641 } 642 loop++; 643 644 // Only do any work in this iteration if we need to. 645 if (transitionIndexDelta > 0 && clampTop 646 || transitionIndexDelta < 0 && clampBottom) { 647 continue; 648 } 649 650 // Obtain the OffsetInterval to use. 651 int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta; 652 OffsetInterval offsetInterval = 653 OffsetInterval.create(zoneInfo, currentTransitionIndex); 654 if (offsetInterval == null) { 655 // No transition exists with the index we tried: Stop searching in the 656 // current direction. 657 clampTop |= (transitionIndexDelta > 0); 658 clampBottom |= (transitionIndexDelta < 0); 659 continue; 660 } 661 662 // Match the wallTimeSeconds against the OffsetInterval. 663 if (mustMatchDst) { 664 // Work out if the interval contains the wall time the caller specified and 665 // matches their isDst value. 666 if (offsetInterval.containsWallTime(wallTimeSeconds)) { 667 if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) { 668 // This always returns the first OffsetInterval it finds that matches 669 // the wall time and isDst requirements. If this.isDst == -1 this means 670 // the result might be a DST or a non-DST answer for wall times that can 671 // exist in two OffsetIntervals. 672 int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds(); 673 int returnValue = checkedSubtract(wallTimeSeconds, 674 totalOffsetSeconds); 675 676 copyFieldsFromCalendar(); 677 this.isDst = offsetInterval.getIsDst(); 678 this.gmtOffsetSeconds = totalOffsetSeconds; 679 return returnValue; 680 } 681 } 682 } else { 683 // To retain similar behavior to the old native implementation: if the caller is 684 // asserting the same isDst value as the OffsetInterval we are looking at we do 685 // not try to find an adjustment from another OffsetInterval of the same isDst 686 // type. If you remove this you get different results in situations like a 687 // DST -> DST transition or STD -> STD transition that results in an interval of 688 // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two 689 // DST intervals, and the caller has passed isDst == 1, this results in a -1 690 // being returned. 691 if (isDst != offsetInterval.getIsDst()) { 692 final int isDstToFind = isDst; 693 Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds, 694 offsetInterval, currentTransitionIndex, isDstToFind); 695 if (returnValue != null) { 696 return returnValue; 697 } 698 } 699 } 700 701 // See if we can avoid another loop in the current direction. 702 if (transitionIndexDelta > 0) { 703 // If we are searching forward and the OffsetInterval we have ends 704 // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further 705 // forward. 706 boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds 707 > MAX_SEARCH_SECONDS; 708 if (endSearch) { 709 clampTop = true; 710 } 711 } else if (transitionIndexDelta < 0) { 712 boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds() 713 >= MAX_SEARCH_SECONDS; 714 if (endSearch) { 715 // If we are searching backward and the OffsetInterval starts 716 // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any 717 // further backwards. 718 clampBottom = true; 719 } 720 } 721 } while (!(clampTop && clampBottom)); 722 return null; 723 } 724 725 public void setYear(int year) { 726 this.year = year; 727 } 728 729 public void setMonth(int month) { 730 this.month = month; 731 } 732 733 public void setMonthDay(int monthDay) { 734 this.monthDay = monthDay; 735 } 736 737 public void setHour(int hour) { 738 this.hour = hour; 739 } 740 741 public void setMinute(int minute) { 742 this.minute = minute; 743 } 744 745 public void setSecond(int second) { 746 this.second = second; 747 } 748 749 public void setWeekDay(int weekDay) { 750 this.weekDay = weekDay; 751 } 752 753 public void setYearDay(int yearDay) { 754 this.yearDay = yearDay; 755 } 756 757 public void setIsDst(int isDst) { 758 this.isDst = isDst; 759 } 760 761 public void setGmtOffset(int gmtoff) { 762 this.gmtOffsetSeconds = gmtoff; 763 } 764 765 public int getYear() { 766 return year; 767 } 768 769 public int getMonth() { 770 return month; 771 } 772 773 public int getMonthDay() { 774 return monthDay; 775 } 776 777 public int getHour() { 778 return hour; 779 } 780 781 public int getMinute() { 782 return minute; 783 } 784 785 public int getSecond() { 786 return second; 787 } 788 789 public int getWeekDay() { 790 return weekDay; 791 } 792 793 public int getYearDay() { 794 return yearDay; 795 } 796 797 public int getGmtOffset() { 798 return gmtOffsetSeconds; 799 } 800 801 public int getIsDst() { 802 return isDst; 803 } 804 805 private void copyFieldsToCalendar() { 806 calendar.set(Calendar.YEAR, year); 807 calendar.set(Calendar.MONTH, month); 808 calendar.set(Calendar.DAY_OF_MONTH, monthDay); 809 calendar.set(Calendar.HOUR_OF_DAY, hour); 810 calendar.set(Calendar.MINUTE, minute); 811 calendar.set(Calendar.SECOND, second); 812 } 813 814 private void copyFieldsFromCalendar() { 815 year = calendar.get(Calendar.YEAR); 816 month = calendar.get(Calendar.MONTH); 817 monthDay = calendar.get(Calendar.DAY_OF_MONTH); 818 hour = calendar.get(Calendar.HOUR_OF_DAY); 819 minute = calendar.get(Calendar.MINUTE); 820 second = calendar.get(Calendar.SECOND); 821 822 // Calendar uses Sunday == 1. Android Time uses Sunday = 0. 823 weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1; 824 // Calendar enumerates from 1, Android Time enumerates from 0. 825 yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1; 826 } 827 828 /** 829 * Find the transition in the {@code timezone} in effect at {@code timeSeconds}. 830 * 831 * <p>Returns an index in the range -1..timeZone.mTransitions.length - 1. -1 is used to 832 * indicate the time is before the first transition. Other values are an index into 833 * timeZone.mTransitions. 834 */ 835 private static int findTransitionIndex(ZoneInfo timeZone, int timeSeconds) { 836 int matchingRawTransition = Arrays.binarySearch(timeZone.mTransitions, timeSeconds); 837 if (matchingRawTransition < 0) { 838 matchingRawTransition = ~matchingRawTransition - 1; 839 } 840 return matchingRawTransition; 841 } 842 } 843 844 /** 845 * A wall-time representation of a timezone offset interval. 846 * 847 * <p>Wall-time means "as it would appear locally in the timezone in which it applies". 848 * For example in 2007: 849 * PST was a -8:00 offset that ran until Mar 11, 2:00 AM. 850 * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM. 851 * PST was a -8:00 offset and ran from Nov 4, 1:00 AM. 852 * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when 853 * PDT ended and PST began. 854 * 855 * <p>For convenience all wall-time values are represented as the number of seconds since the 856 * beginning of the Unix epoch <em>in UTC</em>. To convert from a wall-time to the actual time 857 * in the offset it is necessary to <em>subtract</em> the {@code totalOffsetSeconds}. 858 * For example: If the offset in PST is -07:00 hours, then: 859 * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds 860 * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST 861 */ 862 static class OffsetInterval { 863 864 private final int startWallTimeSeconds; 865 private final int endWallTimeSeconds; 866 private final int isDst; 867 private final int totalOffsetSeconds; 868 869 /** 870 * Creates an {@link OffsetInterval}. 871 * 872 * <p>If {@code transitionIndex} is -1, the transition is synthesized to be a non-DST offset 873 * that runs from the beginning of time until the first transition in {@code timeZone} and 874 * has an offset of {@code timezone.mRawOffset}. If {@code transitionIndex} is the last 875 * transition that transition is considered to run until the end of representable time. 876 * Otherwise, the information is extracted from {@code timeZone.mTransitions}, 877 * {@code timeZone.mOffsets} an {@code timeZone.mIsDsts}. 878 */ 879 public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex) 880 throws CheckedArithmeticException { 881 882 if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) { 883 return null; 884 } 885 886 int rawOffsetSeconds = timeZone.mRawOffset / 1000; 887 if (transitionIndex == -1) { 888 int endWallTimeSeconds = checkedAdd(timeZone.mTransitions[0], rawOffsetSeconds); 889 return new OffsetInterval(Integer.MIN_VALUE, endWallTimeSeconds, 0 /* isDst */, 890 rawOffsetSeconds); 891 } 892 893 byte type = timeZone.mTypes[transitionIndex]; 894 int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds; 895 int endWallTimeSeconds; 896 if (transitionIndex == timeZone.mTransitions.length - 1) { 897 // If this is the last transition, make up the end time. 898 endWallTimeSeconds = Integer.MAX_VALUE; 899 } else { 900 endWallTimeSeconds = checkedAdd(timeZone.mTransitions[transitionIndex + 1], 901 totalOffsetSeconds); 902 } 903 int isDst = timeZone.mIsDsts[type]; 904 int startWallTimeSeconds = 905 checkedAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds); 906 return new OffsetInterval( 907 startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); 908 } 909 910 private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst, 911 int totalOffsetSeconds) { 912 this.startWallTimeSeconds = startWallTimeSeconds; 913 this.endWallTimeSeconds = endWallTimeSeconds; 914 this.isDst = isDst; 915 this.totalOffsetSeconds = totalOffsetSeconds; 916 } 917 918 public boolean containsWallTime(long wallTimeSeconds) { 919 return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds; 920 } 921 922 public int getIsDst() { 923 return isDst; 924 } 925 926 public int getTotalOffsetSeconds() { 927 return totalOffsetSeconds; 928 } 929 930 public long getEndWallTimeSeconds() { 931 return endWallTimeSeconds; 932 } 933 934 public long getStartWallTimeSeconds() { 935 return startWallTimeSeconds; 936 } 937 } 938 939 /** 940 * An exception used to indicate an arithmetic overflow or underflow. 941 */ 942 private static class CheckedArithmeticException extends Exception { 943 } 944 945 /** 946 * Calculate (a + b). 947 * 948 * @throws CheckedArithmeticException if overflow or underflow occurs 949 */ 950 private static int checkedAdd(int a, int b) throws CheckedArithmeticException { 951 // Adapted from Guava IntMath.checkedAdd(); 952 long result = (long) a + b; 953 if (result != (int) result) { 954 throw new CheckedArithmeticException(); 955 } 956 return (int) result; 957 } 958 959 /** 960 * Calculate (a - b). 961 * 962 * @throws CheckedArithmeticException if overflow or underflow occurs 963 */ 964 private static int checkedSubtract(int a, int b) throws CheckedArithmeticException { 965 // Adapted from Guava IntMath.checkedSubtract(); 966 long result = (long) a - b; 967 if (result != (int) result) { 968 throw new CheckedArithmeticException(); 969 } 970 return (int) result; 971 } 972} 973