/*
* Copyright (C) 2006 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.
*/
package android.text.format;
import android.util.TimeFormatException;
import java.io.IOException;
import java.util.Locale;
import java.util.TimeZone;
import libcore.util.ZoneInfo;
import libcore.util.ZoneInfoDB;
/**
* An alternative to the {@link java.util.Calendar} and
* {@link java.util.GregorianCalendar} classes. An instance of the Time class represents
* a moment in time, specified with second precision. It is modelled after
* struct tm. This class is not thread-safe and does not consider leap seconds.
*
* <p>This class has a number of issues and it is recommended that
* {@link java.util.GregorianCalendar} is used instead.
*
* <p>Known issues:
* <ul>
* <li>For historical reasons when performing time calculations all arithmetic currently takes
* place using 32-bit integers. This limits the reliable time range representable from 1902
* until 2037.See the wikipedia article on the
* <a href="http://en.wikipedia.org/wiki/Year_2038_problem">Year 2038 problem</a> for details.
* Do not rely on this behavior; it may change in the future.
* </li>
* <li>Calling {@link #switchTimezone(String)} on a date that cannot exist, such as a wall time
* that was skipped due to a DST transition, will result in a date in 1969 (i.e. -1, or 1 second
* before 1st Jan 1970 UTC).</li>
* <li>Much of the formatting / parsing assumes ASCII text and is therefore not suitable for
* use with non-ASCII scripts.</li>
* <li>No support for pseudo-zones like "GMT-07:00".</li>
* </ul>
*
* @deprecated Use {@link java.util.GregorianCalendar} instead.
*/
@Deprecated
public class Time {
private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000";
private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z";
private static final String Y_M_D = "%Y-%m-%d";
public static final String TIMEZONE_UTC = "UTC";
/**
* The Julian day of the epoch, that is, January 1, 1970 on the Gregorian
* calendar.
*/
public static final int EPOCH_JULIAN_DAY = 2440588;
/**
* The Julian day of the Monday in the week of the epoch, December 29, 1969
* on the Gregorian calendar.
*/
public static final int MONDAY_BEFORE_JULIAN_EPOCH = EPOCH_JULIAN_DAY - 3;
/**
* True if this is an allDay event. The hour, minute, second fields are
* all zero, and the date is displayed the same in all time zones.
*/
public boolean allDay;
/**
* Seconds [0-61] (2 leap seconds allowed)
*/
public int second;
/**
* Minute [0-59]
*/
public int minute;
/**
* Hour of day [0-23]
*/
public int hour;
/**
* Day of month [1-31]
*/
public int monthDay;
/**
* Month [0-11]
*/
public int month;
/**
* Year. For example, 1970.
*/
public int year;
/**
* Day of week [0-6]
*/
public int weekDay;
/**
* Day of year [0-365]
*/
public int yearDay;
/**
* This time is in daylight savings time. One of:
* <ul>
* <li><b>positive</b> - in dst</li>
* <li><b>0</b> - not in dst</li>
* <li><b>negative</b> - unknown</li>
* </ul>
*/
public int isDst;
/**
* Offset in seconds from UTC including any DST offset.
*/
public long gmtoff;
/**
* The timezone for this Time. Should not be null.
*/
public String timezone;
/*
* Define symbolic constants for accessing the fields in this class. Used in
* getActualMaximum().
*/
public static final int SECOND = 1;
public static final int MINUTE = 2;
public static final int HOUR = 3;
public static final int MONTH_DAY = 4;
public static final int MONTH = 5;
public static final int YEAR = 6;
public static final int WEEK_DAY = 7;
public static final int YEAR_DAY = 8;
public static final int WEEK_NUM = 9;
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
// An object that is reused for date calculations.
private TimeCalculator calculator;
/**
* Construct a Time object in the timezone named by the string
* argument "timezone". The time is initialized to Jan 1, 1970.
* @param timezoneId string containing the timezone to use.
* @see TimeZone
*/
public Time(String timezoneId) {
if (timezoneId == null) {
throw new NullPointerException("timezoneId is null!");
}
initialize(timezoneId);
}
/**
* Construct a Time object in the default timezone. The time is initialized to
* Jan 1, 1970.
*/
public Time() {
initialize(TimeZone.getDefault().getID());
}
/**
* A copy constructor. Construct a Time object by copying the given
* Time object. No normalization occurs.
*
* @param other
*/
public Time(Time other) {
initialize(other.timezone);
set(other);
}
/** Initialize the Time to 00:00:00 1/1/1970 in the specified timezone. */
private void initialize(String timezoneId) {
this.timezone = timezoneId;
this.year = 1970;
this.monthDay = 1;
// Set the daylight-saving indicator to the unknown value -1 so that
// it will be recomputed.
this.isDst = -1;
// A reusable object that performs the date/time calculations.
calculator = new TimeCalculator(timezoneId);
}
/**
* Ensures the values in each field are in range. For example if the
* current value of this calendar is March 32, normalize() will convert it
* to April 1. It also fills in weekDay, yearDay, isDst and gmtoff.
*
* <p>
* If "ignoreDst" is true, then this method sets the "isDst" field to -1
* (the "unknown" value) before normalizing. It then computes the
* time in milliseconds and sets the correct value for "isDst" if the
* fields resolve to a valid date / time.
*
* <p>
* See {@link #toMillis(boolean)} for more information about when to
* use <tt>true</tt> or <tt>false</tt> for "ignoreDst" and when {@code -1}
* might be returned.
*
* @return the UTC milliseconds since the epoch, or {@code -1}
*/
public long normalize(boolean ignoreDst) {
calculator.copyFieldsFromTime(this);
long timeInMillis = calculator.toMillis(ignoreDst);
calculator.copyFieldsToTime(this);
return timeInMillis;
}
/**
* Convert this time object so the time represented remains the same, but is
* instead located in a different timezone. This method automatically calls
* normalize() in some cases.
*
* <p>This method can return incorrect results if the date / time cannot be normalized.
*/
public void switchTimezone(String timezone) {
calculator.copyFieldsFromTime(this);
calculator.switchTimeZone(timezone);
calculator.copyFieldsToTime(this);
this.timezone = timezone;
}
private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31,
31, 30, 31, 30, 31 };
/**
* Return the maximum possible value for the given field given the value of
* the other fields. Requires that it be normalized for MONTH_DAY and
* YEAR_DAY.
* @param field one of the constants for HOUR, MINUTE, SECOND, etc.
* @return the maximum value for the field.
*/
public int getActualMaximum(int field) {
switch (field) {
case SECOND:
return 59; // leap seconds, bah humbug
case MINUTE:
return 59;
case HOUR:
return 23;
case MONTH_DAY: {
int n = DAYS_PER_MONTH[this.month];
if (n != 28) {
return n;
} else {
int y = this.year;
return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 29 : 28;
}
}
case MONTH:
return 11;
case YEAR:
return 2037;
case WEEK_DAY:
return 6;
case YEAR_DAY: {
int y = this.year;
// Year days are numbered from 0, so the last one is usually 364.
return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 365 : 364;
}
case WEEK_NUM:
throw new RuntimeException("WEEK_NUM not implemented");
default:
throw new RuntimeException("bad field=" + field);
}
}
/**
* Clears all values, setting the timezone to the given timezone. Sets isDst
* to a negative value to mean "unknown".
* @param timezoneId the timezone to use.
*/
public void clear(String timezoneId) {
if (timezoneId == null) {
throw new NullPointerException("timezone is null!");
}
this.timezone = timezoneId;
this.allDay = false;
this.second = 0;
this.minute = 0;
this.hour = 0;
this.monthDay = 0;
this.month = 0;
this.year = 0;
this.weekDay = 0;
this.yearDay = 0;
this.gmtoff = 0;
this.isDst = -1;
}
/**
* Compare two {@code Time} objects and return a negative number if {@code
* a} is less than {@code b}, a positive number if {@code a} is greater than
* {@code b}, or 0 if they are equal.
*
* <p>
* This method can return an incorrect answer when the date / time fields of
* either {@code Time} have been set to a local time that contradicts the
* available timezone information.
*
* @param a first {@code Time} instance to compare
* @param b second {@code Time} instance to compare
* @throws NullPointerException if either argument is {@code null}
* @throws IllegalArgumentException if {@link #allDay} is true but {@code
* hour}, {@code minute}, and {@code second} are not 0.
* @return a negative result if {@code a} is earlier, a positive result if
* {@code a} is earlier, or 0 if they are equal.
*/
public static int compare(Time a, Time b) {
if (a == null) {
throw new NullPointerException("a == null");
} else if (b == null) {
throw new NullPointerException("b == null");
}
a.calculator.copyFieldsFromTime(a);
b.calculator.copyFieldsFromTime(b);
return TimeCalculator.compare(a.calculator, b.calculator);
}
/**
* Print the current value given the format string provided. See man
* strftime for what means what. The final string must be less than 256
* characters.
* @param format a string containing the desired format.
* @return a String containing the current time expressed in the current locale.
*/
public String format(String format) {
calculator.copyFieldsFromTime(this);
return calculator.format(format);
}
/**
* Return the current time in YYYYMMDDTHHMMSS<tz> format
*/
@Override
public String toString() {
// toString() uses its own TimeCalculator rather than the shared one. Otherwise crazy stuff
// happens during debugging when the debugger calls toString().
TimeCalculator calculator = new TimeCalculator(this.timezone);
calculator.copyFieldsFromTime(this);
return calculator.toStringInternal();
}
/**
* Parses a date-time string in either the RFC 2445 format or an abbreviated
* format that does not include the "time" field. For example, all of the
* following strings are valid:
*
* <ul>
* <li>"20081013T160000Z"</li>
* <li>"20081013T160000"</li>
* <li>"20081013"</li>
* </ul>
*
* Returns whether or not the time is in UTC (ends with Z). If the string
* ends with "Z" then the timezone is set to UTC. If the date-time string
* included only a date and no time field, then the <code>allDay</code>
* field of this Time class is set to true and the <code>hour</code>,
* <code>minute</code>, and <code>second</code> fields are set to zero;
* otherwise (a time field was included in the date-time string)
* <code>allDay</code> is set to false. The fields <code>weekDay</code>,
* <code>yearDay</code>, and <code>gmtoff</code> are always set to zero,
* and the field <code>isDst</code> is set to -1 (unknown). To set those
* fields, call {@link #normalize(boolean)} after parsing.
*
* To parse a date-time string and convert it to UTC milliseconds, do
* something like this:
*
* <pre>
* Time time = new Time();
* String date = "20081013T160000Z";
* time.parse(date);
* long millis = time.normalize(false);
* </pre>
*
* @param s the string to parse
* @return true if the resulting time value is in UTC time
* @throws android.util.TimeFormatException if s cannot be parsed.
*/
public boolean parse(String s) {
if (s == null) {
throw new NullPointerException("time string is null");
}
if (parseInternal(s)) {
timezone = TIMEZONE_UTC;
return true;
}
return false;
}
/**
* Parse a time in the current zone in YYYYMMDDTHHMMSS format.
*/
private boolean parseInternal(String s) {
int len = s.length();
if (len < 8) {
throw new TimeFormatException("String is too short: \"" + s +
"\" Expected at least 8 characters.");
}
boolean inUtc = false;
// year
int n = getChar(s, 0, 1000);
n += getChar(s, 1, 100);
n += getChar(s, 2, 10);
n += getChar(s, 3, 1);
year = n;
// month
n = getChar(s, 4, 10);
n += getChar(s, 5, 1);
n--;
month = n;
// day of month
n = getChar(s, 6, 10);
n += getChar(s, 7, 1);
monthDay = n;
if (len > 8) {
if (len < 15) {
throw new TimeFormatException(
"String is too short: \"" + s
+ "\" If there are more than 8 characters there must be at least"
+ " 15.");
}
checkChar(s, 8, 'T');
allDay = false;
// hour
n = getChar(s, 9, 10);
n += getChar(s, 10, 1);
hour = n;
// min
n = getChar(s, 11, 10);
n += getChar(s, 12, 1);
minute = n;
// sec
n = getChar(s, 13, 10);
n += getChar(s, 14, 1);
second = n;
if (len > 15) {
// Z
checkChar(s, 15, 'Z');
inUtc = true;
}
} else {
allDay = true;
hour = 0;
minute = 0;
second = 0;
}
weekDay = 0;
yearDay = 0;
isDst = -1;
gmtoff = 0;
return inUtc;
}
private void checkChar(String s, int spos, char expected) {
char c = s.charAt(spos);
if (c != expected) {
throw new TimeFormatException(String.format(
"Unexpected character 0x%02d at pos=%d. Expected 0x%02d (\'%c\').",
(int) c, spos, (int) expected, expected));
}
}
private static int getChar(String s, int spos, int mul) {
char c = s.charAt(spos);
if (Character.isDigit(c)) {
return Character.getNumericValue(c) * mul;
} else {
throw new TimeFormatException("Parse error at pos=" + spos);
}
}
/**
* Parse a time in RFC 3339 format. This method also parses simple dates
* (that is, strings that contain no time or time offset). For example,
* all of the following strings are valid:
*
* <ul>
* <li>"2008-10-13T16:00:00.000Z"</li>
* <li>"2008-10-13T16:00:00.000+07:00"</li>
* <li>"2008-10-13T16:00:00.000-07:00"</li>
* <li>"2008-10-13"</li>
* </ul>
*
* <p>
* If the string contains a time and time offset, then the time offset will
* be used to convert the time value to UTC.
* </p>
*
* <p>
* If the given string contains just a date (with no time field), then
* the {@link #allDay} field is set to true and the {@link #hour},
* {@link #minute}, and {@link #second} fields are set to zero.
* </p>
*
* <p>
* Returns true if the resulting time value is in UTC time.
* </p>
*
* @param s the string to parse
* @return true if the resulting time value is in UTC time
* @throws android.util.TimeFormatException if s cannot be parsed.
*/
public boolean parse3339(String s) {
if (s == null) {
throw new NullPointerException("time string is null");
}
if (parse3339Internal(s)) {
timezone = TIMEZONE_UTC;
return true;
}
return false;
}
private boolean parse3339Internal(String s) {
int len = s.length();
if (len < 10) {
throw new TimeFormatException("String too short --- expected at least 10 characters.");
}
boolean inUtc = false;
// year
int n = getChar(s, 0, 1000);
n += getChar(s, 1, 100);
n += getChar(s, 2, 10);
n += getChar(s, 3, 1);
year = n;
checkChar(s, 4, '-');
// month
n = getChar(s, 5, 10);
n += getChar(s, 6, 1);
--n;
month = n;
checkChar(s, 7, '-');
// day
n = getChar(s, 8, 10);
n += getChar(s, 9, 1);
monthDay = n;
if (len >= 19) {
// T
checkChar(s, 10, 'T');
allDay = false;
// hour
n = getChar(s, 11, 10);
n += getChar(s, 12, 1);
// Note that this.hour is not set here. It is set later.
int hour = n;
checkChar(s, 13, ':');
// minute
n = getChar(s, 14, 10);
n += getChar(s, 15, 1);
// Note that this.minute is not set here. It is set later.
int minute = n;
checkChar(s, 16, ':');
// second
n = getChar(s, 17, 10);
n += getChar(s, 18, 1);
second = n;
// skip the '.XYZ' -- we don't care about subsecond precision.
int tzIndex = 19;
if (tzIndex < len && s.charAt(tzIndex) == '.') {
do {
tzIndex++;
} while (tzIndex < len && Character.isDigit(s.charAt(tzIndex)));
}
int offset = 0;
if (len > tzIndex) {
char c = s.charAt(tzIndex);
// NOTE: the offset is meant to be subtracted to get from local time
// to UTC. we therefore use 1 for '-' and -1 for '+'.
switch (c) {
case 'Z':
// Zulu time -- UTC
offset = 0;
break;
case '-':
offset = 1;
break;
case '+':
offset = -1;
break;
default:
throw new TimeFormatException(String.format(
"Unexpected character 0x%02d at position %d. Expected + or -",
(int) c, tzIndex));
}
inUtc = true;
if (offset != 0) {
if (len < tzIndex + 6) {
throw new TimeFormatException(
String.format("Unexpected length; should be %d characters",
tzIndex + 6));
}
// hour
n = getChar(s, tzIndex + 1, 10);
n += getChar(s, tzIndex + 2, 1);
n *= offset;
hour += n;
// minute
n = getChar(s, tzIndex + 4, 10);
n += getChar(s, tzIndex + 5, 1);
n *= offset;
minute += n;
}
}
this.hour = hour;
this.minute = minute;
if (offset != 0) {
normalize(false);
}
} else {
allDay = true;
this.hour = 0;
this.minute = 0;
this.second = 0;
}
this.weekDay = 0;
this.yearDay = 0;
this.isDst = -1;
this.gmtoff = 0;
return inUtc;
}
/**
* Returns the timezone string that is currently set for the device.
*/
public static String getCurrentTimezone() {
return TimeZone.getDefault().getID();
}
/**
* Sets the time of the given Time object to the current time.
*/
public void setToNow() {
set(System.currentTimeMillis());
}
/**
* Converts this time to milliseconds. Suitable for interacting with the
* standard java libraries. The time is in UTC milliseconds since the epoch.
* This does an implicit normalization to compute the milliseconds but does
* <em>not</em> change any of the fields in this Time object. If you want
* to normalize the fields in this Time object and also get the milliseconds
* then use {@link #normalize(boolean)}.
*
* <p>
* If "ignoreDst" is false, then this method uses the current setting of the
* "isDst" field and will adjust the returned time if the "isDst" field is
* wrong for the given time. See the sample code below for an example of
* this.
*
* <p>
* If "ignoreDst" is true, then this method ignores the current setting of
* the "isDst" field in this Time object and will instead figure out the
* correct value of "isDst" (as best it can) from the fields in this
* Time object. The only case where this method cannot figure out the
* correct value of the "isDst" field is when the time is inherently
* ambiguous because it falls in the hour that is repeated when switching
* from Daylight-Saving Time to Standard Time.
*
* <p>
* Here is an example where <tt>toMillis(true)</tt> adjusts the time,
* assuming that DST changes at 2am on Sunday, Nov 4, 2007.
*
* <pre>
* Time time = new Time();
* time.set(4, 10, 2007); // set the date to Nov 4, 2007, 12am
* time.normalize(false); // this sets isDst = 1
* time.monthDay += 1; // changes the date to Nov 5, 2007, 12am
* millis = time.toMillis(false); // millis is Nov 4, 2007, 11pm
* millis = time.toMillis(true); // millis is Nov 5, 2007, 12am
* </pre>
*
* <p>
* To avoid this problem, use <tt>toMillis(true)</tt>
* after adding or subtracting days or explicitly setting the "monthDay"
* field. On the other hand, if you are adding
* or subtracting hours or minutes, then you should use
* <tt>toMillis(false)</tt>.
*
* <p>
* You should also use <tt>toMillis(false)</tt> if you want
* to read back the same milliseconds that you set with {@link #set(long)}
*
* <p>
* This method can return {@code -1} when the date / time fields have been
* set to a local time that conflicts with available timezone information.
* For example, when daylight savings transitions cause an hour to be
* skipped: times within that hour will return {@code -1} if isDst =
* {@code -1}.
*
* or {@link #set(Time)} or after parsing a date string.
*/
public long toMillis(boolean ignoreDst) {
calculator.copyFieldsFromTime(this);
return calculator.toMillis(ignoreDst);
}
/**
* Sets the fields in this Time object given the UTC milliseconds. After
* this method returns, all the fields are normalized.
* This also sets the "isDst" field to the correct value.
*
* @param millis the time in UTC milliseconds since the epoch.
*/
public void set(long millis) {
allDay = false;
calculator.timezone = timezone;
calculator.setTimeInMillis(millis);
calculator.copyFieldsToTime(this);
}
/**
* Format according to RFC 2445 DATE-TIME type.
*
* <p>The same as format("%Y%m%dT%H%M%S"), or format("%Y%m%dT%H%M%SZ") for a Time with a
* timezone set to "UTC".
*/
public String format2445() {
calculator.copyFieldsFromTime(this);
return calculator.format2445(!allDay);
}
/**
* Copy the value of that to this Time object. No normalization happens.
*/
public void set(Time that) {
this.timezone = that.timezone;
this.allDay = that.allDay;
this.second = that.second;
this.minute = that.minute;
this.hour = that.hour;
this.monthDay = that.monthDay;
this.month = that.month;
this.year = that.year;
this.weekDay = that.weekDay;
this.yearDay = that.yearDay;
this.isDst = that.isDst;
this.gmtoff = that.gmtoff;
}
/**
* Sets the fields. Sets weekDay, yearDay and gmtoff to 0, and isDst to -1.
* Call {@link #normalize(boolean)} if you need those.
*/
public void set(int second, int minute, int hour, int monthDay, int month, int year) {
this.allDay = false;
this.second = second;
this.minute = minute;
this.hour = hour;
this.monthDay = monthDay;
this.month = month;
this.year = year;
this.weekDay = 0;
this.yearDay = 0;
this.isDst = -1;
this.gmtoff = 0;
}
/**
* Sets the date from the given fields. Also sets allDay to true.
* Sets weekDay, yearDay and gmtoff to 0, and isDst to -1.
* Call {@link #normalize(boolean)} if you need those.
*
* @param monthDay the day of the month (in the range [1,31])
* @param month the zero-based month number (in the range [0,11])
* @param year the year
*/
public void set(int monthDay, int month, int year) {
this.allDay = true;
this.second = 0;
this.minute = 0;
this.hour = 0;
this.monthDay = monthDay;
this.month = month;
this.year = year;
this.weekDay = 0;
this.yearDay = 0;
this.isDst = -1;
this.gmtoff = 0;
}
/**
* Returns true if the time represented by this Time object occurs before
* the given time.
*
* <p>
* Equivalent to {@code Time.compare(this, that) < 0}. See
* {@link #compare(Time, Time)} for details.
*
* @param that a given Time object to compare against
* @return true if this time is less than the given time
*/
public boolean before(Time that) {
return Time.compare(this, that) < 0;
}
/**
* Returns true if the time represented by this Time object occurs after
* the given time.
*
* <p>
* Equivalent to {@code Time.compare(this, that) > 0}. See
* {@link #compare(Time, Time)} for details.
*
* @param that a given Time object to compare against
* @return true if this time is greater than the given time
*/
public boolean after(Time that) {
return Time.compare(this, that) > 0;
}
/**
* This array is indexed by the weekDay field (SUNDAY=0, MONDAY=1, etc.)
* and gives a number that can be added to the yearDay to give the
* closest Thursday yearDay.
*/
private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 };
/**
* Computes the week number according to ISO 8601. The current Time
* object must already be normalized because this method uses the
* yearDay and weekDay fields.
*
* <p>
* In IS0 8601, weeks start on Monday.
* The first week of the year (week 1) is defined by ISO 8601 as the
* first week with four or more of its days in the starting year.
* Or equivalently, the week containing January 4. Or equivalently,
* the week with the year's first Thursday in it.
* </p>
*
* <p>
* The week number can be calculated by counting Thursdays. Week N
* contains the Nth Thursday of the year.
* </p>
*
* @return the ISO week number.
*/
public int getWeekNumber() {
// Get the year day for the closest Thursday
int closestThursday = yearDay + sThursdayOffset[weekDay];
// Year days start at 0
if (closestThursday >= 0 && closestThursday <= 364) {
return closestThursday / 7 + 1;
}
// The week crosses a year boundary.
Time temp = new Time(this);
temp.monthDay += sThursdayOffset[weekDay];
temp.normalize(true /* ignore isDst */);
return temp.yearDay / 7 + 1;
}
/**
* Return a string in the RFC 3339 format.
* <p>
* If allDay is true, expresses the time as Y-M-D</p>
* <p>
* Otherwise, if the timezone is UTC, expresses the time as Y-M-D-T-H-M-S UTC</p>
* <p>
* Otherwise the time is expressed the time as Y-M-D-T-H-M-S +- GMT</p>
* @return string in the RFC 3339 format.
*/
public String format3339(boolean allDay) {
if (allDay) {
return format(Y_M_D);
} else if (TIMEZONE_UTC.equals(timezone)) {
return format(Y_M_D_T_H_M_S_000_Z);
} else {
String base = format(Y_M_D_T_H_M_S_000);
String sign = (gmtoff < 0) ? "-" : "+";
int offset = (int) Math.abs(gmtoff);
int minutes = (offset % 3600) / 60;
int hours = offset / 3600;
return String.format(Locale.US, "%s%s%02d:%02d", base, sign, hours, minutes);
}
}
/**
* Returns true if the day of the given time is the epoch on the Julian Calendar
* (January 1, 1970 on the Gregorian calendar).
*
* <p>
* This method can return an incorrect answer when the date / time fields have
* been set to a local time that contradicts the available timezone information.
*
* @param time the time to test
* @return true if epoch.
*/
public static boolean isEpoch(Time time) {
long millis = time.toMillis(true);
return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY;
}
/**
* Computes the Julian day number for a point in time in a particular
* timezone. The Julian day for a given date is the same for every
* timezone. For example, the Julian day for July 1, 2008 is 2454649.
*
* <p>Callers must pass the time in UTC millisecond (as can be returned
* by {@link #toMillis(boolean)} or {@link #normalize(boolean)})
* and the offset from UTC of the timezone in seconds (as might be in
* {@link #gmtoff}).
*
* <p>The Julian day is useful for testing if two events occur on the
* same calendar date and for determining the relative time of an event
* from the present ("yesterday", "3 days ago", etc.).
*
* @param millis the time in UTC milliseconds
* @param gmtoff the offset from UTC in seconds
* @return the Julian day
*/
public static int getJulianDay(long millis, long gmtoff) {
long offsetMillis = gmtoff * 1000;
long julianDay = (millis + offsetMillis) / DateUtils.DAY_IN_MILLIS;
return (int) julianDay + EPOCH_JULIAN_DAY;
}
/**
* <p>Sets the time from the given Julian day number, which must be based on
* the same timezone that is set in this Time object. The "gmtoff" field
* need not be initialized because the given Julian day may have a different
* GMT offset than whatever is currently stored in this Time object anyway.
* After this method returns all the fields will be normalized and the time
* will be set to 12am at the beginning of the given Julian day.
* </p>
*
* <p>
* The only exception to this is if 12am does not exist for that day because
* of daylight saving time. For example, Cairo, Eqypt moves time ahead one
* hour at 12am on April 25, 2008 and there are a few other places that
* also change daylight saving time at 12am. In those cases, the time
* will be set to 1am.
* </p>
*
* @param julianDay the Julian day in the timezone for this Time object
* @return the UTC milliseconds for the beginning of the Julian day
*/
public long setJulianDay(int julianDay) {
// Don't bother with the GMT offset since we don't know the correct
// value for the given Julian day. Just get close and then adjust
// the day.
long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS;
set(millis);
// Figure out how close we are to the requested Julian day.
// We can't be off by more than a day.
int approximateDay = getJulianDay(millis, gmtoff);
int diff = julianDay - approximateDay;
monthDay += diff;
// Set the time to 12am and re-normalize.
hour = 0;
minute = 0;
second = 0;
millis = normalize(true);
return millis;
}
/**
* Returns the week since {@link #EPOCH_JULIAN_DAY} (Jan 1, 1970) adjusted
* for first day of week. This takes a julian day and the week start day and
* calculates which week since {@link #EPOCH_JULIAN_DAY} that day occurs in,
* starting at 0. *Do not* use this to compute the ISO week number for the
* year.
*
* @param julianDay The julian day to calculate the week number for
* @param firstDayOfWeek Which week day is the first day of the week, see
* {@link #SUNDAY}
* @return Weeks since the epoch
*/
public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
int diff = THURSDAY - firstDayOfWeek;
if (diff < 0) {
diff += 7;
}
int refDay = EPOCH_JULIAN_DAY - diff;
return (julianDay - refDay) / 7;
}
/**
* Takes a number of weeks since the epoch and calculates the Julian day of
* the Monday for that week. This assumes that the week containing the
* {@link #EPOCH_JULIAN_DAY} is considered week 0. It returns the Julian day
* for the Monday week weeks after the Monday of the week containing the
* epoch.
*
* @param week Number of weeks since the epoch
* @return The julian day for the Monday of the given week since the epoch
*/
public static int getJulianMondayFromWeeksSinceEpoch(int week) {
return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
}
/**
* A class that handles date/time calculations.
*
* This class originated as a port of a native C++ class ("android.Time") to pure Java. It is
* separate from the enclosing class because some methods copy the result of calculations back
* to the enclosing object, but others do not: thus separate state is retained.
*/
private static class TimeCalculator {
public final ZoneInfo.WallTime wallTime;
public String timezone;
// Information about the current timezone.
private ZoneInfo zoneInfo;
public TimeCalculator(String timezoneId) {
this.zoneInfo = lookupZoneInfo(timezoneId);
this.wallTime = new ZoneInfo.WallTime();
}
public long toMillis(boolean ignoreDst) {
if (ignoreDst) {
wallTime.setIsDst(-1);
}
int r = wallTime.mktime(zoneInfo);
if (r == -1) {
return -1;
}
return r * 1000L;
}
public void setTimeInMillis(long millis) {
// Preserve old 32-bit Android behavior.
int intSeconds = (int) (millis / 1000);
updateZoneInfoFromTimeZone();
wallTime.localtime(intSeconds, zoneInfo);
}
public String format(String format) {
if (format == null) {
format = "%c";
}
TimeFormatter formatter = new TimeFormatter();
return formatter.format(format, wallTime, zoneInfo);
}
private void updateZoneInfoFromTimeZone() {
if (!zoneInfo.getID().equals(timezone)) {
this.zoneInfo = lookupZoneInfo(timezone);
}
}
private static ZoneInfo lookupZoneInfo(String timezoneId) {
try {
ZoneInfo zoneInfo = ZoneInfoDB.getInstance().makeTimeZone(timezoneId);
if (zoneInfo == null) {
zoneInfo = ZoneInfoDB.getInstance().makeTimeZone("GMT");
}
if (zoneInfo == null) {
throw new AssertionError("GMT not found: \"" + timezoneId + "\"");
}
return zoneInfo;
} catch (IOException e) {
// This should not ever be thrown.
throw new AssertionError("Error loading timezone: \"" + timezoneId + "\"", e);
}
}
public void switchTimeZone(String timezone) {
int seconds = wallTime.mktime(zoneInfo);
this.timezone = timezone;
updateZoneInfoFromTimeZone();
wallTime.localtime(seconds, zoneInfo);
}
public String format2445(boolean hasTime) {
char[] buf = new char[hasTime ? 16 : 8];
int n = wallTime.getYear();
buf[0] = toChar(n / 1000);
n %= 1000;
buf[1] = toChar(n / 100);
n %= 100;
buf[2] = toChar(n / 10);
n %= 10;
buf[3] = toChar(n);
n = wallTime.getMonth() + 1;
buf[4] = toChar(n / 10);
buf[5] = toChar(n % 10);
n = wallTime.getMonthDay();
buf[6] = toChar(n / 10);
buf[7] = toChar(n % 10);
if (!hasTime) {
return new String(buf, 0, 8);
}
buf[8] = 'T';
n = wallTime.getHour();
buf[9] = toChar(n / 10);
buf[10] = toChar(n % 10);
n = wallTime.getMinute();
buf[11] = toChar(n / 10);
buf[12] = toChar(n % 10);
n = wallTime.getSecond();
buf[13] = toChar(n / 10);
buf[14] = toChar(n % 10);
if (TIMEZONE_UTC.equals(timezone)) {
// The letter 'Z' is appended to the end.
buf[15] = 'Z';
return new String(buf, 0, 16);
} else {
return new String(buf, 0, 15);
}
}
private char toChar(int n) {
return (n >= 0 && n <= 9) ? (char) (n + '0') : ' ';
}
/**
* A method that will return the state of this object in string form. Note: it has side
* effects and so has deliberately not been made the default {@link #toString()}.
*/
public String toStringInternal() {
// This implementation possibly displays the un-normalized fields because that is
// what it has always done.
return String.format("%04d%02d%02dT%02d%02d%02d%s(%d,%d,%d,%d,%d)",
wallTime.getYear(),
wallTime.getMonth() + 1,
wallTime.getMonthDay(),
wallTime.getHour(),
wallTime.getMinute(),
wallTime.getSecond(),
timezone,
wallTime.getWeekDay(),
wallTime.getYearDay(),
wallTime.getGmtOffset(),
wallTime.getIsDst(),
toMillis(false /* use isDst */) / 1000
);
}
public static int compare(TimeCalculator aObject, TimeCalculator bObject) {
if (aObject.timezone.equals(bObject.timezone)) {
// If the timezones are the same, we can easily compare the two times.
int diff = aObject.wallTime.getYear() - bObject.wallTime.getYear();
if (diff != 0) {
return diff;
}
diff = aObject.wallTime.getMonth() - bObject.wallTime.getMonth();
if (diff != 0) {
return diff;
}
diff = aObject.wallTime.getMonthDay() - bObject.wallTime.getMonthDay();
if (diff != 0) {
return diff;
}
diff = aObject.wallTime.getHour() - bObject.wallTime.getHour();
if (diff != 0) {
return diff;
}
diff = aObject.wallTime.getMinute() - bObject.wallTime.getMinute();
if (diff != 0) {
return diff;
}
diff = aObject.wallTime.getSecond() - bObject.wallTime.getSecond();
if (diff != 0) {
return diff;
}
return 0;
} else {
// Otherwise, convert to milliseconds and compare that. This requires that object be
// normalized. Note: For dates that do not exist: toMillis() can return -1, which
// can be confused with a valid time.
long am = aObject.toMillis(false /* use isDst */);
long bm = bObject.toMillis(false /* use isDst */);
long diff = am - bm;
return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0);
}
}
public void copyFieldsToTime(Time time) {
time.second = wallTime.getSecond();
time.minute = wallTime.getMinute();
time.hour = wallTime.getHour();
time.monthDay = wallTime.getMonthDay();
time.month = wallTime.getMonth();
time.year = wallTime.getYear();
// Read-only fields that are derived from other information above.
time.weekDay = wallTime.getWeekDay();
time.yearDay = wallTime.getYearDay();
// < 0: DST status unknown, 0: is not in DST, 1: is in DST
time.isDst = wallTime.getIsDst();
// This is in seconds and includes any DST offset too.
time.gmtoff = wallTime.getGmtOffset();
}
public void copyFieldsFromTime(Time time) {
wallTime.setSecond(time.second);
wallTime.setMinute(time.minute);
wallTime.setHour(time.hour);
wallTime.setMonthDay(time.monthDay);
wallTime.setMonth(time.month);
wallTime.setYear(time.year);
wallTime.setWeekDay(time.weekDay);
wallTime.setYearDay(time.yearDay);
wallTime.setIsDst(time.isDst);
wallTime.setGmtOffset((int) time.gmtoff);
if (time.allDay && (time.second != 0 || time.minute != 0 || time.hour != 0)) {
throw new IllegalArgumentException("allDay is true but sec, min, hour are not 0.");
}
timezone = time.timezone;
updateZoneInfoFromTimeZone();
}
}
}