summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvichang <vichang@google.com>2020-11-06 18:01:08 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2020-11-06 18:01:08 +0000
commit187c1dd8607200c0eb6d253ae7086786a352d2c7 (patch)
treef6aee57e1dd7c5a85a7b3b2643f496200f2ad5e5
parent964e0e09ef0aab4d0f4939dd25fb5e87633435df (diff)
parent44f2d68c406cf2cab9d62f80002c3fb6c4a28912 (diff)
downloadicu-androidx-enterprise-release.tar.gz
Merge "Move inner class WallTime from ZoneInfoData"androidx-enterprise-release
-rw-r--r--android_icu4j/api/legacy_platform/current.txt12
-rw-r--r--android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/WallTime.java726
-rw-r--r--android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/ZoneInfoData.java740
-rw-r--r--android_icu4j/testing/src/com/android/i18n/test/timezone/ZoneInfoDataTest.java4
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);
}