/* * 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. * *
This class has a number of issues and it is recommended that * {@link java.util.GregorianCalendar} is used instead. * *
Known issues: *
* If "ignoreDst" is true, then this method sets the "isDst" field to -1 * (the "unknown" value) before normalizing. It then computes the * correct value for "isDst". * *
* See {@link #toMillis(boolean)} for more information about when to * use true or false for "ignoreDst". * * @return the UTC milliseconds since the epoch */ 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. * *
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.
*
* @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
* If the string contains a time and time offset, then the time offset will
* be used to convert the time value to UTC.
*
* 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.
*
* Returns true if the resulting time value is in UTC time.
*
* 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.
*
*
* 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.
*
*
* Here is an example where toMillis(true) adjusts the time,
* assuming that DST changes at 2am on Sunday, Nov 4, 2007.
*
*
* To avoid this problem, use toMillis(true)
* 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
* toMillis(false).
*
*
* You should also use toMillis(false) if you want
* to read back the same milliseconds that you set with {@link #set(long)}
* 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.
*
* 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.
*
* @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.
*
* @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.
*
*
* 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.
*
* The week number can be calculated by counting Thursdays. Week N
* contains the Nth Thursday of the year.
*
* If allDay is true, expresses the time as Y-M-D
* Otherwise, if the timezone is UTC, expresses the time as Y-M-D-T-H-M-S UTC
* Otherwise the time is expressed the time as Y-M-D-T-H-M-S +- GMT 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}).
*
* 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;
}
/**
* 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.
*
* 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.
*
*
*
* 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 allDay
* field of this Time class is set to true and the hour
,
* minute
, and second
fields are set to zero;
* otherwise (a time field was included in the date-time string)
* allDay
is set to false. The fields weekDay
,
* yearDay
, and gmtoff
are always set to zero,
* and the field isDst
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:
*
*
* Time time = new Time();
* String date = "20081013T160000Z";
* time.parse(date);
* long millis = time.normalize(false);
*
*
* @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:
*
*
*
*
*
* 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
*
*
*