727 lines
31 KiB
Java
727 lines
31 KiB
Java
/*
|
|
* 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.getRawOffset() / 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.getEarliestRawOffset() / 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.getRawOffset() / 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.getRawOffset() / 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.getEarliestRawOffset() / 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;
|
|
}
|
|
}
|