/* * 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.content.res.Resources; import java.util.Locale; import java.util.TimeZone; import libcore.icu.LocaleData; /** * 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, and in fact, uses struct tm to implement most of the * functionality. */ 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: *
* 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
*/
native public long normalize(boolean ignoreDst);
/**
* 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
*/
native public void switchTimezone(String 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 timezone the timezone to use.
*/
public void clear(String timezone) {
if (timezone == null) {
throw new NullPointerException("timezone is null!");
}
this.timezone = timezone;
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");
}
return nativeCompare(a, b);
}
private static native int nativeCompare(Time a, Time b);
/**
* 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) {
synchronized (Time.class) {
Locale locale = Locale.getDefault();
if (sLocale == null || locale == null || !(locale.equals(sLocale))) {
LocaleData localeData = LocaleData.get(locale);
sAm = localeData.amPm[0];
sPm = localeData.amPm[1];
sZeroDigit = localeData.zeroDigit;
sShortMonths = localeData.shortMonthNames;
sLongMonths = localeData.longMonthNames;
sLongStandaloneMonths = localeData.longStandAloneMonthNames;
sShortWeekdays = localeData.shortWeekdayNames;
sLongWeekdays = localeData.longWeekdayNames;
Resources r = Resources.getSystem();
sTimeOnlyFormat = r.getString(com.android.internal.R.string.time_of_day);
sDateOnlyFormat = r.getString(com.android.internal.R.string.month_day_year);
sDateTimeFormat = r.getString(com.android.internal.R.string.date_and_time);
sLocale = locale;
}
String result = format1(format);
if (sZeroDigit != '0') {
result = localizeDigits(result);
}
return result;
}
}
native private String format1(String format);
// TODO: unify this with java.util.Formatter's copy.
private String localizeDigits(String s) {
int length = s.length();
int offsetToLocalizedDigits = sZeroDigit - '0';
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; ++i) {
char ch = s.charAt(i);
if (ch >= '0' && ch <= '9') {
ch += offsetToLocalizedDigits;
}
result.append(ch);
}
return result.toString();
}
/**
* 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.
*/
native public long toMillis(boolean 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.
*/
native public void set(long millis);
/**
* Format according to RFC 2445 DATETIME type.
*
*
* The same as format("%Y%m%dT%H%M%S").
*/
native public String format2445();
/**
* 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
* Use {@link #toMillis(boolean)} to get the milliseconds.
*
* @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 (nativeParse(s)) {
timezone = TIMEZONE_UTC;
return true;
}
return false;
}
/**
* Parse a time in the current zone in YYYYMMDDTHHMMSS format.
*/
native private boolean nativeParse(String s);
/**
* 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
*
*
*