diff options
author | vichang <vichang@google.com> | 2020-11-06 18:01:08 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-11-06 18:01:08 +0000 |
commit | 187c1dd8607200c0eb6d253ae7086786a352d2c7 (patch) | |
tree | f6aee57e1dd7c5a85a7b3b2643f496200f2ad5e5 | |
parent | 964e0e09ef0aab4d0f4939dd25fb5e87633435df (diff) | |
parent | 44f2d68c406cf2cab9d62f80002c3fb6c4a28912 (diff) | |
download | icu-androidx-enterprise-release.tar.gz |
Merge "Move inner class WallTime from ZoneInfoData"androidx-enterprise-release
4 files changed, 751 insertions, 731 deletions
diff --git a/android_icu4j/api/legacy_platform/current.txt b/android_icu4j/api/legacy_platform/current.txt index 3754020f7..a85ca2779 100644 --- a/android_icu4j/api/legacy_platform/current.txt +++ b/android_icu4j/api/legacy_platform/current.txt @@ -156,12 +156,8 @@ package com.android.i18n.timezone { ctor public TzDataSetVersion.TzDataSetException(String, Throwable); } - public final class ZoneInfoData { - method public String getID(); - } - - public static class ZoneInfoData.WallTime { - ctor public ZoneInfoData.WallTime(); + public class WallTime { + ctor public WallTime(); method public int getGmtOffset(); method public int getHour(); method public int getIsDst(); @@ -186,6 +182,10 @@ package com.android.i18n.timezone { method public void setYearDay(int); } + public final class ZoneInfoData { + method public String getID(); + } + public final class ZoneInfoDb implements java.lang.AutoCloseable { method public static com.android.i18n.timezone.ZoneInfoDb getInstance(); method public String getVersion(); diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/WallTime.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/WallTime.java new file mode 100644 index 000000000..2de3f922c --- /dev/null +++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/WallTime.java @@ -0,0 +1,726 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the + * following header: + * + * This file is in the public domain, so clarified as of + * 1996-06-05 by Arthur David Olson. + */ +package com.android.i18n.timezone; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * A class that represents a "wall time". This class is modeled on the C tm struct and + * is used to support android.text.format.Time behavior. Unlike the tm struct the year is + * represented as the full year, not the years since 1900. + * + * <p>This class contains a rewrite of various native functions that android.text.format.Time + * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap + * seconds but does try to preserve behavior around ambiguous date/times found in the BSD + * version of mktime that was previously used. + * + * <p>The original native code used a 32-bit value for time_t on 32-bit Android, which + * was the only variant of Android available at the time. To preserve old behavior this code + * deliberately uses {@code int} rather than {@code long} for most things and performs + * calculations in seconds. This creates deliberate truncation issues for date / times before + * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons + * can be resolved: Application code may have come to rely on the range so previously values + * like zero for year could indicate an invalid date but if we move to long the year zero would + * be valid. + * + * <p>All offsets are considered to be safe for addition / subtraction / multiplication without + * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow. + * + * @hide + */ +@libcore.api.CorePlatformApi +public class WallTime { + + // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic + // and to convert from a broken-down date/time to a millis value. + // Unfortunately, it cannot represent an initial state with a zero day and would + // automatically normalize it, so we must copy values into and out of it as needed. + private final GregorianCalendar calendar; + + private int year; + private int month; + private int monthDay; + private int hour; + private int minute; + private int second; + private int weekDay; + private int yearDay; + private int isDst; + private int gmtOffsetSeconds; + + @libcore.api.CorePlatformApi + public WallTime() { + this.calendar = new GregorianCalendar(0, 0, 0, 0, 0, 0); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + /** + * Sets the wall time to a point in time using the time zone information provided. This + * is a replacement for the old native localtime_tz() function. + * + * <p>When going from an instant to a wall time it is always unambiguous because there + * is only one offset rule acting at any given instant. We do not consider leap seconds. + */ + @libcore.api.CorePlatformApi + public void localtime(int timeSeconds, ZoneInfoData zoneInfo) { + try { + int offsetSeconds = zoneInfo.mRawOffset / 1000; + + // Find out the timezone DST state and adjustment. + byte isDst; + if (zoneInfo.mTransitions.length == 0) { + isDst = 0; + } else { + // offsetIndex can be in the range -1..zoneInfo.mOffsets.length - 1 + int offsetIndex = zoneInfo.findOffsetIndexForTimeInSeconds(timeSeconds); + if (offsetIndex == -1) { + // -1 means timeSeconds is "before the first recorded transition". The first + // recorded transition is treated as a transition from non-DST and the + // earliest known raw offset. + offsetSeconds = zoneInfo.mEarliestRawOffset / 1000; + isDst = 0; + } else { + offsetSeconds += zoneInfo.mOffsets[offsetIndex]; + isDst = zoneInfo.mIsDsts[offsetIndex]; + } + } + + // Perform arithmetic that might underflow before setting fields. + int wallTimeSeconds = checked32BitAdd(timeSeconds, offsetSeconds); + + // Set fields. + calendar.setTimeInMillis(wallTimeSeconds * 1000L); + copyFieldsFromCalendar(); + this.isDst = isDst; + this.gmtOffsetSeconds = offsetSeconds; + } catch (CheckedArithmeticException e) { + // Just stop, leaving fields untouched. + } + } + + /** + * Returns the time in seconds since beginning of the Unix epoch for the wall time using the + * time zone information provided. This is a replacement for an old native mktime_tz() C + * function. + * + * <p>When going from a wall time to an instant the answer can be ambiguous. A wall + * time can map to zero, one or two instants given rational date/time transitions. Rational + * in this case means that transitions occur less frequently than the offset + * differences between them (which could cause all sorts of craziness like the + * skipping out of transitions). + * + * <p>For example, this is not fully supported: + * <ul> + * <li>t1 { time = 1, offset = 0 } + * <li>t2 { time = 2, offset = -1 } + * <li>t3 { time = 3, offset = -2 } + * </ul> + * A wall time in this case might map to t1, t2 or t3. + * + * <p>We do not handle leap seconds. + * <p>We assume that no timezone offset transition has an absolute offset > 24 hours. + * <p>We do not assume that adjacent transitions modify the DST state; adjustments can + * occur for other reasons such as when a zone changes its raw offset. + */ + @libcore.api.CorePlatformApi + public int mktime(ZoneInfoData zoneInfo) { + // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below. + this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0; + + copyFieldsToCalendar(); + final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000; + if (Integer.MIN_VALUE > longWallTimeSeconds + || longWallTimeSeconds > Integer.MAX_VALUE) { + // For compatibility with the old native 32-bit implementation we must treat + // this as an error. Note: -1 could be confused with a real time. + return -1; + } + + try { + final int wallTimeSeconds = (int) longWallTimeSeconds; + final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; + final int rawTimeSeconds = checked32BitSubtract(wallTimeSeconds, rawOffsetSeconds); + + if (zoneInfo.mTransitions.length == 0) { + // There is no transition information. There is just a raw offset for all time. + if (this.isDst > 0) { + // Caller has asserted DST, but there is no DST information available. + return -1; + } + copyFieldsFromCalendar(); + this.isDst = 0; + this.gmtOffsetSeconds = rawOffsetSeconds; + return rawTimeSeconds; + } + + // We cannot know for sure what instant the wall time will map to. Unfortunately, in + // order to know for sure we need the timezone information, but to get the timezone + // information we need an instant. To resolve this we use the raw offset to find an + // OffsetInterval; this will get us the OffsetInterval we need or very close. + + // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1 + // indicates the rawTime is before the first transition and is handled gracefully by + // createOffsetInterval(). + final int initialTransitionIndex = zoneInfo.findTransitionIndex(rawTimeSeconds); + + if (isDst < 0) { + // This is treated as a special case to get it out of the way: + // When a caller has set isDst == -1 it means we can return the first match for + // the wall time we find. If the caller has specified a wall time that cannot + // exist this always returns -1. + + Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, + wallTimeSeconds, true /* mustMatchDst */); + return result == null ? -1 : result; + } + + // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice: + // 1) The first attempts to find a DST offset that matches isDst exactly. + // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see + // if a valid wall time can be created. The result can be somewhat arbitrary. + + Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, + true /* mustMatchDst */); + if (result == null) { + result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, + false /* mustMatchDst */); + } + if (result == null) { + result = -1; + } + return result; + } catch (CheckedArithmeticException e) { + return -1; + } + } + + /** + * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in + * {@code targetInterval}. + * + * <p>This is used when a caller has made an assertion about standard time / DST that cannot + * be matched to any offset interval that exists. We must therefore assume that the isDst + * assertion is incorrect and the invalid wall time is the result of some modification the + * caller made to a valid wall time that pushed them outside of the offset interval they + * were in. We must correct for any DST change that should have been applied when they did + * so. + * + * <p>Unfortunately, we have no information about what adjustment they made and so cannot + * know which offset interval they were previously in. For example, they may have added a + * second or a year to a valid time to arrive at what they have. + * + * <p>We try all offset types that are not the same as the isDst the caller asserted. For + * each possible offset we work out the offset difference between that and + * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If + * we are, then we have found an adjustment. + */ + private Integer tryOffsetAdjustments(ZoneInfoData zoneInfo, int oldWallTimeSeconds, + OffsetInterval targetInterval, int transitionIndex, int isDstToFind) + throws CheckedArithmeticException { + + int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind); + for (int j = 0; j < offsetsToTry.length; j++) { + int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; + int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j]; + int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds(); + int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds; + int adjustedWallTimeSeconds = checked32BitAdd(oldWallTimeSeconds, adjustmentSeconds); + if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) { + // Perform any arithmetic that might overflow. + int returnValue = checked32BitSubtract(adjustedWallTimeSeconds, + targetIntervalOffsetSeconds); + + // Modify field state and return the result. + calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L); + copyFieldsFromCalendar(); + this.isDst = targetInterval.getIsDst(); + this.gmtOffsetSeconds = targetIntervalOffsetSeconds; + return returnValue; + } + } + return null; + } + + /** + * Return an array of offsets that have the requested {@code isDst} value. + * The {@code startIndex} is used as a starting point so transitions nearest + * to that index are returned first. + */ + private static int[] getOffsetsOfType(ZoneInfoData zoneInfo, int startIndex, int isDst) { + // +1 to account for the synthetic transition we invent before the first recorded one. + int[] offsets = new int[zoneInfo.mOffsets.length + 1]; + boolean[] seen = new boolean[zoneInfo.mOffsets.length]; + int numFound = 0; + + int delta = 0; + boolean clampTop = false; + boolean clampBottom = false; + do { + // delta = { 1, -1, 2, -2, 3, -3...} + delta *= -1; + if (delta >= 0) { + delta++; + } + + int transitionIndex = startIndex + delta; + if (delta < 0 && transitionIndex < -1) { + clampBottom = true; + continue; + } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) { + clampTop = true; + continue; + } + + if (transitionIndex == -1) { + if (isDst == 0) { + // Synthesize a non-DST transition before the first transition we have + // data for. + offsets[numFound++] = 0; // offset of 0 from raw offset + } + continue; + } + int type = zoneInfo.mTypes[transitionIndex] & 0xff; + if (!seen[type]) { + if (zoneInfo.mIsDsts[type] == isDst) { + offsets[numFound++] = zoneInfo.mOffsets[type]; + } + seen[type] = true; + } + } while (!(clampTop && clampBottom)); + + int[] toReturn = new int[numFound]; + System.arraycopy(offsets, 0, toReturn, 0, numFound); + return toReturn; + } + + /** + * Find a time <em>in seconds</em> the same or close to {@code wallTimeSeconds} that + * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition + * with {@code initialTransitionIndex}. + * + * <p>If {@code mustMatchDst} is {@code true} the method can only return times that + * use timezone offsets that satisfy the {@code this.isDst} requirements. + * If {@code this.isDst == -1} it means that any offset can be used. + * + * <p>If {@code mustMatchDst} is {@code false} any offset that covers the + * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset + * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}. + * + * <p>Note: This method both uses and can modify field state. It returns the matching time + * in seconds if a match has been found and modifies fields, or it returns {@code null} and + * leaves the field state unmodified. + */ + private Integer doWallTimeSearch(ZoneInfoData zoneInfo, int initialTransitionIndex, + int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException { + + // The loop below starts at the initialTransitionIndex and radiates out from that point + // up to 24 hours in either direction by applying transitionIndexDelta to inspect + // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no + // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to + // indicate whether the search has either searched > 24 hours or exhausted the + // transition data in that direction. The search stops when a match is found or if + // clampTop and clampBottom are both true. + // The match logic employed is determined by the mustMatchDst parameter. + final int MAX_SEARCH_SECONDS = 24 * 60 * 60; + boolean clampTop = false, clampBottom = false; + int loop = 0; + do { + // transitionIndexDelta = { 0, -1, 1, -2, 2,..} + int transitionIndexDelta = (loop + 1) / 2; + if (loop % 2 == 1) { + transitionIndexDelta *= -1; + } + loop++; + + // Only do any work in this iteration if we need to. + if (transitionIndexDelta > 0 && clampTop + || transitionIndexDelta < 0 && clampBottom) { + continue; + } + + // Obtain the OffsetInterval to use. + int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta; + OffsetInterval offsetInterval = + OffsetInterval.create(zoneInfo, currentTransitionIndex); + if (offsetInterval == null) { + // No transition exists with the index we tried: Stop searching in the + // current direction. + clampTop |= (transitionIndexDelta > 0); + clampBottom |= (transitionIndexDelta < 0); + continue; + } + + // Match the wallTimeSeconds against the OffsetInterval. + if (mustMatchDst) { + // Work out if the interval contains the wall time the caller specified and + // matches their isDst value. + if (offsetInterval.containsWallTime(wallTimeSeconds)) { + if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) { + // This always returns the first OffsetInterval it finds that matches + // the wall time and isDst requirements. If this.isDst == -1 this means + // the result might be a DST or a non-DST answer for wall times that can + // exist in two OffsetIntervals. + int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds(); + int returnValue = checked32BitSubtract(wallTimeSeconds, totalOffsetSeconds); + + copyFieldsFromCalendar(); + this.isDst = offsetInterval.getIsDst(); + this.gmtOffsetSeconds = totalOffsetSeconds; + return returnValue; + } + } + } else { + // To retain similar behavior to the old native implementation: if the caller is + // asserting the same isDst value as the OffsetInterval we are looking at we do + // not try to find an adjustment from another OffsetInterval of the same isDst + // type. If you remove this you get different results in situations like a + // DST -> DST transition or STD -> STD transition that results in an interval of + // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two + // DST intervals, and the caller has passed isDst == 1, this results in a -1 + // being returned. + if (isDst != offsetInterval.getIsDst()) { + final int isDstToFind = isDst; + Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds, + offsetInterval, currentTransitionIndex, isDstToFind); + if (returnValue != null) { + return returnValue; + } + } + } + + // See if we can avoid another loop in the current direction. + if (transitionIndexDelta > 0) { + // If we are searching forward and the OffsetInterval we have ends + // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further + // forward. + boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds + > MAX_SEARCH_SECONDS; + if (endSearch) { + clampTop = true; + } + } else if (transitionIndexDelta < 0) { + boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds() + >= MAX_SEARCH_SECONDS; + if (endSearch) { + // If we are searching backward and the OffsetInterval starts + // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any + // further backwards. + clampBottom = true; + } + } + } while (!(clampTop && clampBottom)); + return null; + } + + @libcore.api.CorePlatformApi + public void setYear(int year) { + this.year = year; + } + + @libcore.api.CorePlatformApi + public void setMonth(int month) { + this.month = month; + } + + @libcore.api.CorePlatformApi + public void setMonthDay(int monthDay) { + this.monthDay = monthDay; + } + + @libcore.api.CorePlatformApi + public void setHour(int hour) { + this.hour = hour; + } + + @libcore.api.CorePlatformApi + public void setMinute(int minute) { + this.minute = minute; + } + + @libcore.api.CorePlatformApi + public void setSecond(int second) { + this.second = second; + } + + @libcore.api.CorePlatformApi + public void setWeekDay(int weekDay) { + this.weekDay = weekDay; + } + + @libcore.api.CorePlatformApi + public void setYearDay(int yearDay) { + this.yearDay = yearDay; + } + + @libcore.api.CorePlatformApi + public void setIsDst(int isDst) { + this.isDst = isDst; + } + + @libcore.api.CorePlatformApi + public void setGmtOffset(int gmtoff) { + this.gmtOffsetSeconds = gmtoff; + } + + @libcore.api.CorePlatformApi + public int getYear() { + return year; + } + + @libcore.api.CorePlatformApi + public int getMonth() { + return month; + } + + @libcore.api.CorePlatformApi + public int getMonthDay() { + return monthDay; + } + + @libcore.api.CorePlatformApi + public int getHour() { + return hour; + } + + @libcore.api.CorePlatformApi + public int getMinute() { + return minute; + } + + @libcore.api.CorePlatformApi + public int getSecond() { + return second; + } + + @libcore.api.CorePlatformApi + public int getWeekDay() { + return weekDay; + } + + @libcore.api.CorePlatformApi + public int getYearDay() { + return yearDay; + } + + @libcore.api.CorePlatformApi + public int getGmtOffset() { + return gmtOffsetSeconds; + } + + @libcore.api.CorePlatformApi + public int getIsDst() { + return isDst; + } + + private void copyFieldsToCalendar() { + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, monthDay); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); + } + + private void copyFieldsFromCalendar() { + year = calendar.get(Calendar.YEAR); + month = calendar.get(Calendar.MONTH); + monthDay = calendar.get(Calendar.DAY_OF_MONTH); + hour = calendar.get(Calendar.HOUR_OF_DAY); + minute = calendar.get(Calendar.MINUTE); + second = calendar.get(Calendar.SECOND); + + // Calendar uses Sunday == 1. Android Time uses Sunday = 0. + weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1; + // Calendar enumerates from 1, Android Time enumerates from 0. + yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1; + } + + /** + * A wall-time representation of a timezone offset interval. + * + * <p>Wall-time means "as it would appear locally in the timezone in which it applies". + * For example in 2007: + * PST was a -8:00 offset that ran until Mar 11, 2:00 AM. + * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM. + * PST was a -8:00 offset and ran from Nov 4, 1:00 AM. + * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when + * PDT ended and PST began. + * + * <p>Although wall-time means "local time", for convenience all wall-time values are stored in + * the number of seconds since the beginning of the Unix epoch to get that time <em>in UTC</em>. + * To convert from a wall-time to the actual UTC time it is necessary to <em>subtract</em> the + * {@code totalOffsetSeconds}. + * For example: If the offset in PST is -07:00 hours, then: + * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds + * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST + */ + static class OffsetInterval { + + /** The time the interval starts in seconds since start of epoch, inclusive. */ + private final int startWallTimeSeconds; + /** The time the interval ends in seconds since start of epoch, exclusive. */ + private final int endWallTimeSeconds; + private final int isDst; + private final int totalOffsetSeconds; + + /** + * Creates an {@link OffsetInterval}. + * + * <p>If {@code transitionIndex} is -1, where possible the transition is synthesized to run + * from the beginning of 32-bit time until the first transition in {@code zoneInfo} with + * offset information based on the first type defined. If {@code transitionIndex} is the + * last transition, that transition is considered to run until the end of 32-bit time. + * Otherwise, the information is extracted from {@code zoneInfo.mTransitions}, + * {@code zoneInfo.mOffsets} and {@code zoneInfo.mIsDsts}. + * + * <p>This method can return null when: + * <ol> + * <li>the {@code transitionIndex} is outside the allowed range, i.e. + * {@code transitionIndex < -1 || transitionIndex >= [the number of transitions]}.</li> + * <li>when calculations result in a zero-length interval. This is only expected to occur + * when dealing with transitions close to (or exactly at) {@code Integer.MIN_VALUE} and + * {@code Integer.MAX_VALUE} and where it's difficult to convert from UTC to local times. + * </li> + * </ol> + */ + public static OffsetInterval create(ZoneInfoData zoneInfo, int transitionIndex) { + if (transitionIndex < -1 || transitionIndex >= zoneInfo.mTransitions.length) { + return null; + } + + if (transitionIndex == -1) { + int totalOffsetSeconds = zoneInfo.mEarliestRawOffset / 1000; + int isDst = 0; + + int startWallTimeSeconds = Integer.MIN_VALUE; + int endWallTimeSeconds = + saturated32BitAdd(zoneInfo.mTransitions[0], totalOffsetSeconds); + if (startWallTimeSeconds == endWallTimeSeconds) { + // There's no point in returning an OffsetInterval that lasts 0 seconds. + return null; + } + return new OffsetInterval(startWallTimeSeconds, endWallTimeSeconds, isDst, + totalOffsetSeconds); + } + + int rawOffsetSeconds = zoneInfo.getRawOffset() / 1000; + int type = zoneInfo.mTypes[transitionIndex] & 0xff; + int totalOffsetSeconds = zoneInfo.mOffsets[type] + rawOffsetSeconds; + int endWallTimeSeconds; + if (transitionIndex == zoneInfo.mTransitions.length - 1) { + endWallTimeSeconds = Integer.MAX_VALUE; + } else { + endWallTimeSeconds = saturated32BitAdd( + zoneInfo.mTransitions[transitionIndex + 1], totalOffsetSeconds); + } + int isDst = zoneInfo.mIsDsts[type]; + int startWallTimeSeconds = + saturated32BitAdd(zoneInfo.mTransitions[transitionIndex], totalOffsetSeconds); + if (startWallTimeSeconds == endWallTimeSeconds) { + // There's no point in returning an OffsetInterval that lasts 0 seconds. + return null; + } + return new OffsetInterval( + startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); + } + + private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst, + int totalOffsetSeconds) { + this.startWallTimeSeconds = startWallTimeSeconds; + this.endWallTimeSeconds = endWallTimeSeconds; + this.isDst = isDst; + this.totalOffsetSeconds = totalOffsetSeconds; + } + + public boolean containsWallTime(long wallTimeSeconds) { + return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds; + } + + public int getIsDst() { + return isDst; + } + + public int getTotalOffsetSeconds() { + return totalOffsetSeconds; + } + + public long getEndWallTimeSeconds() { + return endWallTimeSeconds; + } + + public long getStartWallTimeSeconds() { + return startWallTimeSeconds; + } + } + + /** + * An exception used to indicate an arithmetic overflow or underflow. + */ + private static class CheckedArithmeticException extends Exception { + } + + /** + * Calculate (a + b). The result must be in the Integer range otherwise an exception is thrown. + * + * @throws CheckedArithmeticException if overflow or underflow occurs + */ + private static int checked32BitAdd(long a, int b) throws CheckedArithmeticException { + // Adapted from Guava IntMath.checkedAdd(); + long result = a + b; + if (result != (int) result) { + throw new CheckedArithmeticException(); + } + return (int) result; + } + + /** + * Calculate (a - b). The result must be in the Integer range otherwise an exception is thrown. + * + * @throws CheckedArithmeticException if overflow or underflow occurs + */ + private static int checked32BitSubtract(long a, int b) throws CheckedArithmeticException { + // Adapted from Guava IntMath.checkedSubtract(); + long result = a - b; + if (result != (int) result) { + throw new CheckedArithmeticException(); + } + return (int) result; + } + + /** + * Calculate (a + b). If the result would overflow or underflow outside of the Integer range + * Integer.MAX_VALUE or Integer.MIN_VALUE will be returned, respectively. + */ + private static int saturated32BitAdd(long a, int b) { + long result = a + b; + if (result > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else if (result < Integer.MIN_VALUE) { + return Integer.MIN_VALUE; + } + return (int) result; + } +} diff --git a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java index 6cb54b50d..ae9edd196 100644 --- a/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java +++ b/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java @@ -13,13 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the - * following header: - * - * This file is in the public domain, so clarified as of - * 1996-06-05 by Arthur David Olson. - */ package com.android.i18n.timezone; import com.android.i18n.timezone.internal.BufferIterator; @@ -31,10 +24,7 @@ import java.io.ObjectOutputStream; import java.io.ObjectStreamField; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Calendar; import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; /** * Our concrete TimeZone implementation, backed by zoneinfo data. @@ -44,12 +34,14 @@ import java.util.TimeZone; * zone info compiler (zic) tool (see {@code man 5 tzfile} for details of the format and * {@code man 8 zic}) and an index by long name, e.g. Europe/London. * - * <p>The compacted form is created by {@code external/icu/tools/ZoneCompactor.java} and is used - * by both this and Bionic. {@link ZoneInfoDb} is responsible for mapping the binary file, and + * <p>The compacted form is created by + * {@code system/timezone/input_tools/android/zone_compactor/main/java/ZoneCompactor.java} and is + * used by both this and Bionic. {@link ZoneInfoDb} is responsible for mapping the binary file, and * reading the index and creating a {@link BufferIterator} that provides access to an entry for a * specific file. This class is responsible for reading the data from that {@link BufferIterator} - * and storing it a representation to support the {@link TimeZone} and {@link GregorianCalendar} - * implementations. See {@link ZoneInfoData#readTimeZone(String, BufferIterator, long)}. + * and storing it a representation to support the {@link java.util.TimeZone} and + * {@link java.util.GregorianCalendar} implementations. See + * {@link ZoneInfoData#readTimeZone(String, BufferIterator, long)}. * * <p>This class does not use all the information from the {@code tzfile}; it uses: * {@code tzh_timecnt} and the associated transition times and type information. For each type @@ -81,13 +73,13 @@ public final class ZoneInfoData { * The (best guess) non-DST offset used "today". It is stored in milliseconds. * See also {@link #mOffsets} which holds values relative to this value, albeit in seconds. */ - private int mRawOffset; + int mRawOffset; /** * The earliest non-DST offset for the zone. It is stored in milliseconds and is absolute, i.e. * it is not relative to mRawOffset. */ - private final int mEarliestRawOffset; + final int mEarliestRawOffset; /** * Implements {@link #useDaylightTime()} @@ -95,9 +87,9 @@ public final class ZoneInfoData { * <p>True if the transition active at the time this instance was created, or future * transitions support DST. It is possible that caching this value at construction time and * using it for the lifetime of the instance does not match the contract of the - * {@link TimeZone#useDaylightTime()} method but it appears to be what the RI does and that - * method is not particularly useful when it comes to historical or future times as it does not - * allow the time to be specified. + * {@link java.util.TimeZone#useDaylightTime()} method but it appears to be what the RI does + * and that method is not particularly useful when it comes to historical or future times as it + * does not allow the time to be specified. * * <p>When this is false then {@link #mDstSavings} will be 0. * @@ -138,7 +130,7 @@ public final class ZoneInfoData { * * @see #mTypes */ - private final long[] mTransitions; + final long[] mTransitions; /** * The type of the transition, where type is a pair consisting of the offset and whether the @@ -158,7 +150,7 @@ public final class ZoneInfoData { * @see #mOffsets * @see #mIsDsts */ - private final byte[] mTypes; + final byte[] mTypes; /** * The offset parts of the transition types, in seconds. @@ -172,7 +164,7 @@ public final class ZoneInfoData { * @see #mTypes * @see #mIsDsts */ - private final int[] mOffsets; + final int[] mOffsets; /** * Specifies whether an associated offset includes DST or not. @@ -183,7 +175,7 @@ public final class ZoneInfoData { * @see #mTypes * @see #mOffsets */ - private final byte[] mIsDsts; + final byte[] mIsDsts; private ZoneInfoData(String id, int rawOffset, int earliestRawOffset, boolean useDst, int dstSavings, long[] transitions, byte[] types, int[] offsets, byte[] isDsts) { @@ -217,6 +209,7 @@ public final class ZoneInfoData { mIsDsts = that.mIsDsts == null ? null : that.mIsDsts.clone(); } + public static ZoneInfoData readTimeZone(String id, BufferIterator it, long currentTimeMillis) throws IOException { @@ -809,707 +802,6 @@ public final class ZoneInfoData { } /** - * A class that represents a "wall time". This class is modeled on the C tm struct and - * is used to support android.text.format.Time behavior. Unlike the tm struct the year is - * represented as the full year, not the years since 1900. - * - * <p>This class contains a rewrite of various native functions that android.text.format.Time - * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap - * seconds but does try to preserve behavior around ambiguous date/times found in the BSD - * version of mktime that was previously used. - * - * <p>The original native code used a 32-bit value for time_t on 32-bit Android, which - * was the only variant of Android available at the time. To preserve old behavior this code - * deliberately uses {@code int} rather than {@code long} for most things and performs - * calculations in seconds. This creates deliberate truncation issues for date / times before - * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons - * can be resolved: Application code may have come to rely on the range so previously values - * like zero for year could indicate an invalid date but if we move to long the year zero would - * be valid. - * - * <p>All offsets are considered to be safe for addition / subtraction / multiplication without - * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow. - * - * @hide - */ - @libcore.api.CorePlatformApi - public static class WallTime { - - // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic - // and to convert from a broken-down date/time to a millis value. - // Unfortunately, it cannot represent an initial state with a zero day and would - // automatically normalize it, so we must copy values into and out of it as needed. - private final GregorianCalendar calendar; - - private int year; - private int month; - private int monthDay; - private int hour; - private int minute; - private int second; - private int weekDay; - private int yearDay; - private int isDst; - private int gmtOffsetSeconds; - - @libcore.api.CorePlatformApi - public WallTime() { - this.calendar = new GregorianCalendar(0, 0, 0, 0, 0, 0); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - /** - * Sets the wall time to a point in time using the time zone information provided. This - * is a replacement for the old native localtime_tz() function. - * - * <p>When going from an instant to a wall time it is always unambiguous because there - * is only one offset rule acting at any given instant. We do not consider leap seconds. - */ - @libcore.api.CorePlatformApi - public void localtime(int timeSeconds, ZoneInfoData zoneInfo) { - try { - int offsetSeconds = zoneInfo.mRawOffset / 1000; - - // Find out the timezone DST state and adjustment. - byte isDst; - if (zoneInfo.mTransitions.length == 0) { - isDst = 0; - } else { - // offsetIndex can be in the range -1..zoneInfo.mOffsets.length - 1 - int offsetIndex = zoneInfo.findOffsetIndexForTimeInSeconds(timeSeconds); - if (offsetIndex == -1) { - // -1 means timeSeconds is "before the first recorded transition". The first - // recorded transition is treated as a transition from non-DST and the - // earliest known raw offset. - offsetSeconds = zoneInfo.mEarliestRawOffset / 1000; - isDst = 0; - } else { - offsetSeconds += zoneInfo.mOffsets[offsetIndex]; - isDst = zoneInfo.mIsDsts[offsetIndex]; - } - } - - // Perform arithmetic that might underflow before setting fields. - int wallTimeSeconds = checked32BitAdd(timeSeconds, offsetSeconds); - - // Set fields. - calendar.setTimeInMillis(wallTimeSeconds * 1000L); - copyFieldsFromCalendar(); - this.isDst = isDst; - this.gmtOffsetSeconds = offsetSeconds; - } catch (CheckedArithmeticException e) { - // Just stop, leaving fields untouched. - } - } - - /** - * Returns the time in seconds since beginning of the Unix epoch for the wall time using the - * time zone information provided. This is a replacement for an old native mktime_tz() C - * function. - * - * <p>When going from a wall time to an instant the answer can be ambiguous. A wall - * time can map to zero, one or two instants given rational date/time transitions. Rational - * in this case means that transitions occur less frequently than the offset - * differences between them (which could cause all sorts of craziness like the - * skipping out of transitions). - * - * <p>For example, this is not fully supported: - * <ul> - * <li>t1 { time = 1, offset = 0 } - * <li>t2 { time = 2, offset = -1 } - * <li>t3 { time = 3, offset = -2 } - * </ul> - * A wall time in this case might map to t1, t2 or t3. - * - * <p>We do not handle leap seconds. - * <p>We assume that no timezone offset transition has an absolute offset > 24 hours. - * <p>We do not assume that adjacent transitions modify the DST state; adjustments can - * occur for other reasons such as when a zone changes its raw offset. - */ - @libcore.api.CorePlatformApi - public int mktime(ZoneInfoData zoneInfo) { - // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below. - this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0; - - copyFieldsToCalendar(); - final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000; - if (Integer.MIN_VALUE > longWallTimeSeconds - || longWallTimeSeconds > Integer.MAX_VALUE) { - // For compatibility with the old native 32-bit implementation we must treat - // this as an error. Note: -1 could be confused with a real time. - return -1; - } - - try { - final int wallTimeSeconds = (int) longWallTimeSeconds; - final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; - final int rawTimeSeconds = checked32BitSubtract(wallTimeSeconds, rawOffsetSeconds); - - if (zoneInfo.mTransitions.length == 0) { - // There is no transition information. There is just a raw offset for all time. - if (this.isDst > 0) { - // Caller has asserted DST, but there is no DST information available. - return -1; - } - copyFieldsFromCalendar(); - this.isDst = 0; - this.gmtOffsetSeconds = rawOffsetSeconds; - return rawTimeSeconds; - } - - // We cannot know for sure what instant the wall time will map to. Unfortunately, in - // order to know for sure we need the timezone information, but to get the timezone - // information we need an instant. To resolve this we use the raw offset to find an - // OffsetInterval; this will get us the OffsetInterval we need or very close. - - // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1 - // indicates the rawTime is before the first transition and is handled gracefully by - // createOffsetInterval(). - final int initialTransitionIndex = zoneInfo.findTransitionIndex(rawTimeSeconds); - - if (isDst < 0) { - // This is treated as a special case to get it out of the way: - // When a caller has set isDst == -1 it means we can return the first match for - // the wall time we find. If the caller has specified a wall time that cannot - // exist this always returns -1. - - Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, - wallTimeSeconds, true /* mustMatchDst */); - return result == null ? -1 : result; - } - - // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice: - // 1) The first attempts to find a DST offset that matches isDst exactly. - // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see - // if a valid wall time can be created. The result can be somewhat arbitrary. - - Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, - true /* mustMatchDst */); - if (result == null) { - result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, - false /* mustMatchDst */); - } - if (result == null) { - result = -1; - } - return result; - } catch (CheckedArithmeticException e) { - return -1; - } - } - - /** - * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in - * {@code targetInterval}. - * - * <p>This is used when a caller has made an assertion about standard time / DST that cannot - * be matched to any offset interval that exists. We must therefore assume that the isDst - * assertion is incorrect and the invalid wall time is the result of some modification the - * caller made to a valid wall time that pushed them outside of the offset interval they - * were in. We must correct for any DST change that should have been applied when they did - * so. - * - * <p>Unfortunately, we have no information about what adjustment they made and so cannot - * know which offset interval they were previously in. For example, they may have added a - * second or a year to a valid time to arrive at what they have. - * - * <p>We try all offset types that are not the same as the isDst the caller asserted. For - * each possible offset we work out the offset difference between that and - * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If - * we are, then we have found an adjustment. - */ - private Integer tryOffsetAdjustments(ZoneInfoData zoneInfo, int oldWallTimeSeconds, - OffsetInterval targetInterval, int transitionIndex, int isDstToFind) - throws CheckedArithmeticException { - - int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind); - for (int j = 0; j < offsetsToTry.length; j++) { - int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; - int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j]; - int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds(); - int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds; - int adjustedWallTimeSeconds = - checked32BitAdd(oldWallTimeSeconds, adjustmentSeconds); - if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) { - // Perform any arithmetic that might overflow. - int returnValue = checked32BitSubtract(adjustedWallTimeSeconds, - targetIntervalOffsetSeconds); - - // Modify field state and return the result. - calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L); - copyFieldsFromCalendar(); - this.isDst = targetInterval.getIsDst(); - this.gmtOffsetSeconds = targetIntervalOffsetSeconds; - return returnValue; - } - } - return null; - } - - /** - * Return an array of offsets that have the requested {@code isDst} value. - * The {@code startIndex} is used as a starting point so transitions nearest - * to that index are returned first. - */ - private static int[] getOffsetsOfType(ZoneInfoData zoneInfo, int startIndex, int isDst) { - // +1 to account for the synthetic transition we invent before the first recorded one. - int[] offsets = new int[zoneInfo.mOffsets.length + 1]; - boolean[] seen = new boolean[zoneInfo.mOffsets.length]; - int numFound = 0; - - int delta = 0; - boolean clampTop = false; - boolean clampBottom = false; - do { - // delta = { 1, -1, 2, -2, 3, -3...} - delta *= -1; - if (delta >= 0) { - delta++; - } - - int transitionIndex = startIndex + delta; - if (delta < 0 && transitionIndex < -1) { - clampBottom = true; - continue; - } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) { - clampTop = true; - continue; - } - - if (transitionIndex == -1) { - if (isDst == 0) { - // Synthesize a non-DST transition before the first transition we have - // data for. - offsets[numFound++] = 0; // offset of 0 from raw offset - } - continue; - } - int type = zoneInfo.mTypes[transitionIndex] & 0xff; - if (!seen[type]) { - if (zoneInfo.mIsDsts[type] == isDst) { - offsets[numFound++] = zoneInfo.mOffsets[type]; - } - seen[type] = true; - } - } while (!(clampTop && clampBottom)); - - int[] toReturn = new int[numFound]; - System.arraycopy(offsets, 0, toReturn, 0, numFound); - return toReturn; - } - - /** - * Find a time <em>in seconds</em> the same or close to {@code wallTimeSeconds} that - * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition - * with {@code initialTransitionIndex}. - * - * <p>If {@code mustMatchDst} is {@code true} the method can only return times that - * use timezone offsets that satisfy the {@code this.isDst} requirements. - * If {@code this.isDst == -1} it means that any offset can be used. - * - * <p>If {@code mustMatchDst} is {@code false} any offset that covers the - * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset - * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}. - * - * <p>Note: This method both uses and can modify field state. It returns the matching time - * in seconds if a match has been found and modifies fields, or it returns {@code null} and - * leaves the field state unmodified. - */ - private Integer doWallTimeSearch(ZoneInfoData zoneInfo, int initialTransitionIndex, - int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException { - - // The loop below starts at the initialTransitionIndex and radiates out from that point - // up to 24 hours in either direction by applying transitionIndexDelta to inspect - // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no - // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to - // indicate whether the search has either searched > 24 hours or exhausted the - // transition data in that direction. The search stops when a match is found or if - // clampTop and clampBottom are both true. - // The match logic employed is determined by the mustMatchDst parameter. - final int MAX_SEARCH_SECONDS = 24 * 60 * 60; - boolean clampTop = false, clampBottom = false; - int loop = 0; - do { - // transitionIndexDelta = { 0, -1, 1, -2, 2,..} - int transitionIndexDelta = (loop + 1) / 2; - if (loop % 2 == 1) { - transitionIndexDelta *= -1; - } - loop++; - - // Only do any work in this iteration if we need to. - if (transitionIndexDelta > 0 && clampTop - || transitionIndexDelta < 0 && clampBottom) { - continue; - } - - // Obtain the OffsetInterval to use. - int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta; - OffsetInterval offsetInterval = - OffsetInterval.create(zoneInfo, currentTransitionIndex); - if (offsetInterval == null) { - // No transition exists with the index we tried: Stop searching in the - // current direction. - clampTop |= (transitionIndexDelta > 0); - clampBottom |= (transitionIndexDelta < 0); - continue; - } - - // Match the wallTimeSeconds against the OffsetInterval. - if (mustMatchDst) { - // Work out if the interval contains the wall time the caller specified and - // matches their isDst value. - if (offsetInterval.containsWallTime(wallTimeSeconds)) { - if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) { - // This always returns the first OffsetInterval it finds that matches - // the wall time and isDst requirements. If this.isDst == -1 this means - // the result might be a DST or a non-DST answer for wall times that can - // exist in two OffsetIntervals. - int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds(); - int returnValue = - checked32BitSubtract(wallTimeSeconds, totalOffsetSeconds); - - copyFieldsFromCalendar(); - this.isDst = offsetInterval.getIsDst(); - this.gmtOffsetSeconds = totalOffsetSeconds; - return returnValue; - } - } - } else { - // To retain similar behavior to the old native implementation: if the caller is - // asserting the same isDst value as the OffsetInterval we are looking at we do - // not try to find an adjustment from another OffsetInterval of the same isDst - // type. If you remove this you get different results in situations like a - // DST -> DST transition or STD -> STD transition that results in an interval of - // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two - // DST intervals, and the caller has passed isDst == 1, this results in a -1 - // being returned. - if (isDst != offsetInterval.getIsDst()) { - final int isDstToFind = isDst; - Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds, - offsetInterval, currentTransitionIndex, isDstToFind); - if (returnValue != null) { - return returnValue; - } - } - } - - // See if we can avoid another loop in the current direction. - if (transitionIndexDelta > 0) { - // If we are searching forward and the OffsetInterval we have ends - // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further - // forward. - boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds - > MAX_SEARCH_SECONDS; - if (endSearch) { - clampTop = true; - } - } else if (transitionIndexDelta < 0) { - boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds() - >= MAX_SEARCH_SECONDS; - if (endSearch) { - // If we are searching backward and the OffsetInterval starts - // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any - // further backwards. - clampBottom = true; - } - } - } while (!(clampTop && clampBottom)); - return null; - } - - @libcore.api.CorePlatformApi - public void setYear(int year) { - this.year = year; - } - - @libcore.api.CorePlatformApi - public void setMonth(int month) { - this.month = month; - } - - @libcore.api.CorePlatformApi - public void setMonthDay(int monthDay) { - this.monthDay = monthDay; - } - - @libcore.api.CorePlatformApi - public void setHour(int hour) { - this.hour = hour; - } - - @libcore.api.CorePlatformApi - public void setMinute(int minute) { - this.minute = minute; - } - - @libcore.api.CorePlatformApi - public void setSecond(int second) { - this.second = second; - } - - @libcore.api.CorePlatformApi - public void setWeekDay(int weekDay) { - this.weekDay = weekDay; - } - - @libcore.api.CorePlatformApi - public void setYearDay(int yearDay) { - this.yearDay = yearDay; - } - - @libcore.api.CorePlatformApi - public void setIsDst(int isDst) { - this.isDst = isDst; - } - - @libcore.api.CorePlatformApi - public void setGmtOffset(int gmtoff) { - this.gmtOffsetSeconds = gmtoff; - } - - @libcore.api.CorePlatformApi - public int getYear() { - return year; - } - - @libcore.api.CorePlatformApi - public int getMonth() { - return month; - } - - @libcore.api.CorePlatformApi - public int getMonthDay() { - return monthDay; - } - - @libcore.api.CorePlatformApi - public int getHour() { - return hour; - } - - @libcore.api.CorePlatformApi - public int getMinute() { - return minute; - } - - @libcore.api.CorePlatformApi - public int getSecond() { - return second; - } - - @libcore.api.CorePlatformApi - public int getWeekDay() { - return weekDay; - } - - @libcore.api.CorePlatformApi - public int getYearDay() { - return yearDay; - } - - @libcore.api.CorePlatformApi - public int getGmtOffset() { - return gmtOffsetSeconds; - } - - @libcore.api.CorePlatformApi - public int getIsDst() { - return isDst; - } - - private void copyFieldsToCalendar() { - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, month); - calendar.set(Calendar.DAY_OF_MONTH, monthDay); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minute); - calendar.set(Calendar.SECOND, second); - calendar.set(Calendar.MILLISECOND, 0); - } - - private void copyFieldsFromCalendar() { - year = calendar.get(Calendar.YEAR); - month = calendar.get(Calendar.MONTH); - monthDay = calendar.get(Calendar.DAY_OF_MONTH); - hour = calendar.get(Calendar.HOUR_OF_DAY); - minute = calendar.get(Calendar.MINUTE); - second = calendar.get(Calendar.SECOND); - - // Calendar uses Sunday == 1. Android Time uses Sunday = 0. - weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1; - // Calendar enumerates from 1, Android Time enumerates from 0. - yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1; - } - } - - /** - * A wall-time representation of a timezone offset interval. - * - * <p>Wall-time means "as it would appear locally in the timezone in which it applies". - * For example in 2007: - * PST was a -8:00 offset that ran until Mar 11, 2:00 AM. - * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM. - * PST was a -8:00 offset and ran from Nov 4, 1:00 AM. - * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when - * PDT ended and PST began. - * - * <p>Although wall-time means "local time", for convenience all wall-time values are stored in - * the number of seconds since the beginning of the Unix epoch to get that time <em>in UTC</em>. - * To convert from a wall-time to the actual UTC time it is necessary to <em>subtract</em> the - * {@code totalOffsetSeconds}. - * For example: If the offset in PST is -07:00 hours, then: - * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds - * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST - */ - static class OffsetInterval { - - /** The time the interval starts in seconds since start of epoch, inclusive. */ - private final int startWallTimeSeconds; - /** The time the interval ends in seconds since start of epoch, exclusive. */ - private final int endWallTimeSeconds; - private final int isDst; - private final int totalOffsetSeconds; - - /** - * Creates an {@link OffsetInterval}. - * - * <p>If {@code transitionIndex} is -1, where possible the transition is synthesized to run - * from the beginning of 32-bit time until the first transition in {@code timeZone} with - * offset information based on the first type defined. If {@code transitionIndex} is the - * last transition, that transition is considered to run until the end of 32-bit time. - * Otherwise, the information is extracted from {@code timeZone.mTransitions}, - * {@code timeZone.mOffsets} and {@code timeZone.mIsDsts}. - * - * <p>This method can return null when: - * <ol> - * <li>the {@code transitionIndex} is outside the allowed range, i.e. - * {@code transitionIndex < -1 || transitionIndex >= [the number of transitions]}.</li> - * <li>when calculations result in a zero-length interval. This is only expected to occur - * when dealing with transitions close to (or exactly at) {@code Integer.MIN_VALUE} and - * {@code Integer.MAX_VALUE} and where it's difficult to convert from UTC to local times. - * </li> - * </ol> - */ - public static OffsetInterval create(ZoneInfoData timeZone, int transitionIndex) { - if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) { - return null; - } - - if (transitionIndex == -1) { - int totalOffsetSeconds = timeZone.mEarliestRawOffset / 1000; - int isDst = 0; - - int startWallTimeSeconds = Integer.MIN_VALUE; - int endWallTimeSeconds = - saturated32BitAdd(timeZone.mTransitions[0], totalOffsetSeconds); - if (startWallTimeSeconds == endWallTimeSeconds) { - // There's no point in returning an OffsetInterval that lasts 0 seconds. - return null; - } - return new OffsetInterval(startWallTimeSeconds, endWallTimeSeconds, isDst, - totalOffsetSeconds); - } - - int rawOffsetSeconds = timeZone.mRawOffset / 1000; - int type = timeZone.mTypes[transitionIndex] & 0xff; - int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds; - int endWallTimeSeconds; - if (transitionIndex == timeZone.mTransitions.length - 1) { - endWallTimeSeconds = Integer.MAX_VALUE; - } else { - endWallTimeSeconds = saturated32BitAdd( - timeZone.mTransitions[transitionIndex + 1], totalOffsetSeconds); - } - int isDst = timeZone.mIsDsts[type]; - int startWallTimeSeconds = - saturated32BitAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds); - if (startWallTimeSeconds == endWallTimeSeconds) { - // There's no point in returning an OffsetInterval that lasts 0 seconds. - return null; - } - return new OffsetInterval( - startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); - } - - private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst, - int totalOffsetSeconds) { - this.startWallTimeSeconds = startWallTimeSeconds; - this.endWallTimeSeconds = endWallTimeSeconds; - this.isDst = isDst; - this.totalOffsetSeconds = totalOffsetSeconds; - } - - public boolean containsWallTime(long wallTimeSeconds) { - return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds; - } - - public int getIsDst() { - return isDst; - } - - public int getTotalOffsetSeconds() { - return totalOffsetSeconds; - } - - public long getEndWallTimeSeconds() { - return endWallTimeSeconds; - } - - public long getStartWallTimeSeconds() { - return startWallTimeSeconds; - } - } - - /** - * An exception used to indicate an arithmetic overflow or underflow. - */ - private static class CheckedArithmeticException extends Exception { - } - - /** - * Calculate (a + b). The result must be in the Integer range otherwise an exception is thrown. - * - * @throws CheckedArithmeticException if overflow or underflow occurs - */ - private static int checked32BitAdd(long a, int b) throws CheckedArithmeticException { - // Adapted from Guava IntMath.checkedAdd(); - long result = a + b; - if (result != (int) result) { - throw new CheckedArithmeticException(); - } - return (int) result; - } - - /** - * Calculate (a - b). The result must be in the Integer range otherwise an exception is thrown. - * - * @throws CheckedArithmeticException if overflow or underflow occurs - */ - private static int checked32BitSubtract(long a, int b) throws CheckedArithmeticException { - // Adapted from Guava IntMath.checkedSubtract(); - long result = a - b; - if (result != (int) result) { - throw new CheckedArithmeticException(); - } - return (int) result; - } - - /** - * Calculate (a + b). If the result would overflow or underflow outside of the Integer range - * Integer.MAX_VALUE or Integer.MIN_VALUE will be returned, respectively. - */ - private static int saturated32BitAdd(long a, int b) { - long result = a + b; - if (result > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } else if (result < Integer.MIN_VALUE) { - return Integer.MIN_VALUE; - } - return (int) result; - } - - /** * IntraCoreApi made visible for testing in libcore */ @libcore.api.IntraCoreApi diff --git a/android_icu4j/testing/src/com/android/i18n/test/timezone/ZoneInfoDataTest.java b/android_icu4j/testing/src/com/android/i18n/test/timezone/ZoneInfoDataTest.java index 48fe3efaf..346a47fe8 100644 --- a/android_icu4j/testing/src/com/android/i18n/test/timezone/ZoneInfoDataTest.java +++ b/android_icu4j/testing/src/com/android/i18n/test/timezone/ZoneInfoDataTest.java @@ -16,6 +16,8 @@ package com.android.i18n.test.timezone; import android.icu.testsharding.MainTestShard; + +import com.android.i18n.timezone.WallTime; import com.android.i18n.timezone.ZoneInfoData; import com.android.i18n.timezone.ZoneInfoDb; import java.io.IOException; @@ -456,7 +458,7 @@ public class ZoneInfoDataTest extends TestCase { assertDSTSavings(zoneInfoData, offsetFromSeconds(0)); // Make sure that WallTime works properly with a ZoneInfoData with 256 types. - ZoneInfoData.WallTime wallTime = new ZoneInfoData.WallTime(); + WallTime wallTime = new WallTime(); wallTime.localtime(0, zoneInfoData); wallTime.mktime(zoneInfoData); } |