summaryrefslogtreecommitdiffstats
path: root/core/java/android/text/format
diff options
context:
space:
mode:
authorNeil Fuller <nfuller@google.com>2014-06-25 11:13:25 +0100
committerNeil Fuller <nfuller@google.com>2014-07-22 15:06:16 +0100
commitd7f0849b8c053ccc6abf0dc7d5bc07da502782a4 (patch)
tree4588210a2db9878d594423ce47899e640fc47868 /core/java/android/text/format
parent02fd104f303cb7d55d6af42be8fe7be543e9aba5 (diff)
downloadframeworks_base-d7f0849b8c053ccc6abf0dc7d5bc07da502782a4.zip
frameworks_base-d7f0849b8c053ccc6abf0dc7d5bc07da502782a4.tar.gz
frameworks_base-d7f0849b8c053ccc6abf0dc7d5bc07da502782a4.tar.bz2
Rewriting android.text.format.Time without the native _tz functions
Bug: 15765976 Change-Id: I666b72ecf9da8a9dcfb97cc503006b415909a558
Diffstat (limited to 'core/java/android/text/format')
-rw-r--r--core/java/android/text/format/Time.java677
-rw-r--r--core/java/android/text/format/TimeFormatter.java519
2 files changed, 1081 insertions, 115 deletions
diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java
index f34e746..aa6ad20 100644
--- a/core/java/android/text/format/Time.java
+++ b/core/java/android/text/format/Time.java
@@ -16,19 +16,38 @@
package android.text.format;
-import android.content.res.Resources;
+import android.util.TimeFormatException;
+import java.io.IOException;
import java.util.Locale;
import java.util.TimeZone;
-import libcore.icu.LocaleData;
+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, and in fact, uses struct tm to implement most of the
- * functionality.
+ * 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>
+ * </ul>
*/
public class Time {
private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000";
@@ -106,7 +125,7 @@ public class Time {
public int isDst;
/**
- * Offset from UTC (in seconds).
+ * Offset in seconds from UTC including any DST offset.
*/
public long gmtoff;
@@ -137,41 +156,20 @@ public class Time {
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
- /*
- * The Locale for which date formatting strings have been loaded.
- */
- private static Locale sLocale;
- private static String[] sShortMonths;
- private static String[] sLongMonths;
- private static String[] sLongStandaloneMonths;
- private static String[] sShortWeekdays;
- private static String[] sLongWeekdays;
- private static String sTimeOnlyFormat;
- private static String sDateOnlyFormat;
- private static String sDateTimeFormat;
- private static String sAm;
- private static String sPm;
- private static char sZeroDigit;
-
- // Referenced by native code.
- private static String sDateCommand = "%a %b %e %H:%M:%S %Z %Y";
+ // 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 timezone string containing the timezone to use.
+ * @param timezoneId string containing the timezone to use.
* @see TimeZone
*/
- public Time(String timezone) {
- if (timezone == null) {
- throw new NullPointerException("timezone is null!");
+ public Time(String timezoneId) {
+ if (timezoneId == null) {
+ throw new NullPointerException("timezoneId is null!");
}
- this.timezone = timezone;
- 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;
+ initialize(timezoneId);
}
/**
@@ -179,7 +177,7 @@ public class Time {
* Jan 1, 1970.
*/
public Time() {
- this(TimeZone.getDefault().getID());
+ initialize(TimeZone.getDefault().getID());
}
/**
@@ -189,9 +187,23 @@ public class Time {
* @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
@@ -208,14 +220,26 @@ public class Time {
*
* @return the UTC milliseconds since the epoch
*/
- native public long normalize(boolean ignoreDst);
+ 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
+ * normalize() in some cases.
+ *
+ * <p>This method can return incorrect results if the date / time cannot be normalized.
*/
- native public void switchTimezone(String timezone);
+ 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 };
@@ -265,13 +289,13 @@ public class Time {
/**
* 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.
+ * @param timezoneId the timezone to use.
*/
- public void clear(String timezone) {
- if (timezone == null) {
+ public void clear(String timezoneId) {
+ if (timezoneId == null) {
throw new NullPointerException("timezone is null!");
}
- this.timezone = timezone;
+ this.timezone = timezoneId;
this.allDay = false;
this.second = 0;
this.minute = 0;
@@ -304,12 +328,12 @@ public class Time {
} else if (b == null) {
throw new NullPointerException("b == null");
}
+ a.calculator.copyFieldsFromTime(a);
+ b.calculator.copyFieldsFromTime(b);
- return nativeCompare(a, b);
+ return TimeCalculator.compare(a.calculator, b.calculator);
}
- 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
@@ -318,61 +342,21 @@ public class Time {
* @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();
+ calculator.copyFieldsFromTime(this);
+ return calculator.format(format);
}
-
/**
* Return the current time in YYYYMMDDTHHMMSS<tz> format
*/
@Override
- native public String toString();
+ 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
@@ -414,7 +398,7 @@ public class Time {
if (s == null) {
throw new NullPointerException("time string is null");
}
- if (nativeParse(s)) {
+ if (parseInternal(s)) {
timezone = TIMEZONE_UTC;
return true;
}
@@ -424,7 +408,94 @@ public class Time {
/**
* Parse a time in the current zone in YYYYMMDDTHHMMSS format.
*/
- native private boolean nativeParse(String s);
+ 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
@@ -461,14 +532,140 @@ public class Time {
if (s == null) {
throw new NullPointerException("time string is null");
}
- if (nativeParse3339(s)) {
+ if (parse3339Internal(s)) {
timezone = TIMEZONE_UTC;
return true;
}
return false;
}
- native private boolean nativeParse3339(String s);
+ 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.
@@ -480,7 +677,9 @@ public class Time {
/**
* Sets the time of the given Time object to the current time.
*/
- native public void setToNow();
+ public void setToNow() {
+ set(System.currentTimeMillis());
+ }
/**
* Converts this time to milliseconds. Suitable for interacting with the
@@ -530,7 +729,10 @@ public class Time {
* 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);
+ public long toMillis(boolean ignoreDst) {
+ calculator.copyFieldsFromTime(this);
+ return calculator.toMillis(ignoreDst);
+ }
/**
* Sets the fields in this Time object given the UTC milliseconds. After
@@ -539,15 +741,23 @@ public class Time {
*
* @param millis the time in UTC milliseconds since the epoch.
*/
- native public void set(long millis);
+ public void set(long millis) {
+ allDay = false;
+ calculator.timezone = timezone;
+ calculator.setTimeInMillis(millis);
+ calculator.copyFieldsToTime(this);
+ }
/**
- * Format according to RFC 2445 DATETIME type.
+ * Format according to RFC 2445 DATE-TIME type.
*
- * <p>
- * The same as format("%Y%m%dT%H%M%S").
+ * <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".
*/
- native public String format2445();
+ public String format2445() {
+ calculator.copyFieldsFromTime(this);
+ return calculator.format2445(!allDay);
+ }
/**
* Copy the value of that to this Time object. No normalization happens.
@@ -682,7 +892,6 @@ public class Time {
* 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>
- * @param allDay
* @return string in the RFC 3339 format.
*/
public String format3339(boolean allDay) {
@@ -693,7 +902,7 @@ public class Time {
} else {
String base = format(Y_M_D_T_H_M_S_000);
String sign = (gmtoff < 0) ? "-" : "+";
- int offset = (int)Math.abs(gmtoff);
+ int offset = (int) Math.abs(gmtoff);
int minutes = (offset % 3600) / 60;
int hours = offset / 3600;
@@ -714,16 +923,18 @@ public class Time {
}
/**
- * Computes the Julian day number, given the UTC milliseconds
- * and the offset (in seconds) from UTC. The Julian day for a given
- * date will be the same for every timezone. For example, the Julian
- * day for July 1, 2008 is 2454649. This is the same value no matter
- * what timezone is being used. The Julian day is useful for testing
- * if two events occur on the same day and for determining the relative
- * time of an event from the present ("yesterday", "3 days ago", etc.).
+ * 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>
- * Use {@link #toMillis(boolean)} to get the milliseconds.
+ * <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
@@ -810,4 +1021,240 @@ public class Time {
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();
+ }
+ }
}
diff --git a/core/java/android/text/format/TimeFormatter.java b/core/java/android/text/format/TimeFormatter.java
new file mode 100644
index 0000000..ec79b36
--- /dev/null
+++ b/core/java/android/text/format/TimeFormatter.java
@@ -0,0 +1,519 @@
+/*
+ * Based on the UCB version of strftime.c with the copyright notice appearing below.
+ */
+
+/*
+** Copyright (c) 1989 The Regents of the University of California.
+** All rights reserved.
+**
+** Redistribution and use in source and binary forms are permitted
+** provided that the above copyright notice and this paragraph are
+** duplicated in all such forms and that any documentation,
+** advertising materials, and other materials related to such
+** distribution and use acknowledge that the software was developed
+** by the University of California, Berkeley. The name of the
+** University may not be used to endorse or promote products derived
+** from this software without specific prior written permission.
+** THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
+** IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+*/
+package android.text.format;
+
+import android.content.res.Resources;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Formatter;
+import java.util.Locale;
+import java.util.TimeZone;
+import libcore.icu.LocaleData;
+import libcore.util.ZoneInfo;
+
+/**
+ * Formatting logic for {@link Time}. Contains a port of Bionic's broken strftime_tz to Java. The
+ * main issue with this implementation is the treatment of characters as ASCII, despite returning
+ * localized (UTF-16) strings from the LocaleData.
+ *
+ * <p>This class is not thread safe.
+ */
+class TimeFormatter {
+ // An arbitrary value outside the range representable by a byte / ASCII character code.
+ private static final int FORCE_LOWER_CASE = 0x100;
+
+ private static final int SECSPERMIN = 60;
+ private static final int MINSPERHOUR = 60;
+ private static final int DAYSPERWEEK = 7;
+ private static final int MONSPERYEAR = 12;
+ private static final int HOURSPERDAY = 24;
+ private static final int DAYSPERLYEAR = 366;
+ private static final int DAYSPERNYEAR = 365;
+
+ /**
+ * The Locale for which the cached LocaleData and formats have been loaded.
+ */
+ private static Locale sLocale;
+ private static LocaleData sLocaleData;
+ private static String sTimeOnlyFormat;
+ private static String sDateOnlyFormat;
+ private static String sDateTimeFormat;
+
+ private final LocaleData localeData;
+ private final String dateTimeFormat;
+ private final String timeOnlyFormat;
+ private final String dateOnlyFormat;
+ private final Locale locale;
+
+ private StringBuilder outputBuilder;
+ private Formatter outputFormatter;
+
+ public TimeFormatter() {
+ synchronized (TimeFormatter.class) {
+ Locale locale = Locale.getDefault();
+
+ if (sLocale == null || !(locale.equals(sLocale))) {
+ sLocale = locale;
+ sLocaleData = LocaleData.get(locale);
+
+ 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);
+ }
+
+ this.dateTimeFormat = sDateTimeFormat;
+ this.timeOnlyFormat = sTimeOnlyFormat;
+ this.dateOnlyFormat = sDateOnlyFormat;
+ this.locale = locale;
+ localeData = sLocaleData;
+ }
+ }
+
+ /**
+ * Format the specified {@code wallTime} using {@code pattern}. The output is returned.
+ */
+ public String format(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
+ try {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ outputBuilder = stringBuilder;
+ outputFormatter = new Formatter(stringBuilder, locale);
+
+ formatInternal(pattern, wallTime, zoneInfo);
+ String result = stringBuilder.toString();
+ // This behavior is the source of a bug since some formats are defined as being
+ // in ASCII. Generally localization is very broken.
+ if (localeData.zeroDigit != '0') {
+ result = localizeDigits(result);
+ }
+ return result;
+ } finally {
+ outputBuilder = null;
+ outputFormatter = null;
+ }
+ }
+
+ private String localizeDigits(String s) {
+ int length = s.length();
+ int offsetToLocalizedDigits = localeData.zeroDigit - '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();
+ }
+
+ /**
+ * Format the specified {@code wallTime} using {@code pattern}. The output is written to
+ * {@link #outputBuilder}.
+ */
+ private void formatInternal(String pattern, ZoneInfo.WallTime wallTime, ZoneInfo zoneInfo) {
+ // Convert to ASCII bytes to be compatible with old implementation behavior.
+ byte[] bytes = pattern.getBytes(StandardCharsets.US_ASCII);
+ if (bytes.length == 0) {
+ return;
+ }
+
+ ByteBuffer formatBuffer = ByteBuffer.wrap(bytes);
+ while (formatBuffer.remaining() > 0) {
+ boolean outputCurrentByte = true;
+ char currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position()));
+ if (currentByteAsChar == '%') {
+ outputCurrentByte = handleToken(formatBuffer, wallTime, zoneInfo);
+ }
+ if (outputCurrentByte) {
+ currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position()));
+ outputBuilder.append(currentByteAsChar);
+ }
+
+ formatBuffer.position(formatBuffer.position() + 1);
+ }
+ }
+
+ private boolean handleToken(ByteBuffer formatBuffer, ZoneInfo.WallTime wallTime,
+ ZoneInfo zoneInfo) {
+
+ // The byte at formatBuffer.position() is expected to be '%' at this point.
+ int modifier = 0;
+ while (formatBuffer.remaining() > 1) {
+ // Increment the position then get the new current byte.
+ formatBuffer.position(formatBuffer.position() + 1);
+ char currentByteAsChar = convertToChar(formatBuffer.get(formatBuffer.position()));
+ switch (currentByteAsChar) {
+ case 'A':
+ modifyAndAppend((wallTime.getWeekDay() < 0
+ || wallTime.getWeekDay() >= DAYSPERWEEK)
+ ? "?" : localeData.longWeekdayNames[wallTime.getWeekDay() + 1],
+ modifier);
+ return false;
+ case 'a':
+ modifyAndAppend((wallTime.getWeekDay() < 0
+ || wallTime.getWeekDay() >= DAYSPERWEEK)
+ ? "?" : localeData.shortWeekdayNames[wallTime.getWeekDay() + 1],
+ modifier);
+ return false;
+ case 'B':
+ if (modifier == '-') {
+ modifyAndAppend((wallTime.getMonth() < 0
+ || wallTime.getMonth() >= MONSPERYEAR)
+ ? "?"
+ : localeData.longStandAloneMonthNames[wallTime.getMonth()],
+ modifier);
+ } else {
+ modifyAndAppend((wallTime.getMonth() < 0
+ || wallTime.getMonth() >= MONSPERYEAR)
+ ? "?" : localeData.longMonthNames[wallTime.getMonth()],
+ modifier);
+ }
+ return false;
+ case 'b':
+ case 'h':
+ modifyAndAppend((wallTime.getMonth() < 0 || wallTime.getMonth() >= MONSPERYEAR)
+ ? "?" : localeData.shortMonthNames[wallTime.getMonth()],
+ modifier);
+ return false;
+ case 'C':
+ outputYear(wallTime.getYear(), true, false, modifier);
+ return false;
+ case 'c':
+ formatInternal(dateTimeFormat, wallTime, zoneInfo);
+ return false;
+ case 'D':
+ formatInternal("%m/%d/%y", wallTime, zoneInfo);
+ return false;
+ case 'd':
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
+ wallTime.getMonthDay());
+ return false;
+ case 'E':
+ case 'O':
+ // C99 locale modifiers are not supported.
+ continue;
+ case '_':
+ case '-':
+ case '0':
+ case '^':
+ case '#':
+ modifier = currentByteAsChar;
+ continue;
+ case 'e':
+ outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
+ wallTime.getMonthDay());
+ return false;
+ case 'F':
+ formatInternal("%Y-%m-%d", wallTime, zoneInfo);
+ return false;
+ case 'H':
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
+ wallTime.getHour());
+ return false;
+ case 'I':
+ int hour = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), hour);
+ return false;
+ case 'j':
+ int yearDay = wallTime.getYearDay() + 1;
+ outputFormatter.format(getFormat(modifier, "%03d", "%3d", "%d", "%03d"),
+ yearDay);
+ return false;
+ case 'k':
+ outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"),
+ wallTime.getHour());
+ return false;
+ case 'l':
+ int n2 = (wallTime.getHour() % 12 != 0) ? (wallTime.getHour() % 12) : 12;
+ outputFormatter.format(getFormat(modifier, "%2d", "%2d", "%d", "%02d"), n2);
+ return false;
+ case 'M':
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
+ wallTime.getMinute());
+ return false;
+ case 'm':
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
+ wallTime.getMonth() + 1);
+ return false;
+ case 'n':
+ modifyAndAppend("\n", modifier);
+ return false;
+ case 'p':
+ modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
+ : localeData.amPm[0], modifier);
+ return false;
+ case 'P':
+ modifyAndAppend((wallTime.getHour() >= (HOURSPERDAY / 2)) ? localeData.amPm[1]
+ : localeData.amPm[0], FORCE_LOWER_CASE);
+ return false;
+ case 'R':
+ formatInternal("%H:%M", wallTime, zoneInfo);
+ return false;
+ case 'r':
+ formatInternal("%I:%M:%S %p", wallTime, zoneInfo);
+ return false;
+ case 'S':
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
+ wallTime.getSecond());
+ return false;
+ case 's':
+ int timeInSeconds = wallTime.mktime(zoneInfo);
+ modifyAndAppend(Integer.toString(timeInSeconds), modifier);
+ return false;
+ case 'T':
+ formatInternal("%H:%M:%S", wallTime, zoneInfo);
+ return false;
+ case 't':
+ modifyAndAppend("\t", modifier);
+ return false;
+ case 'U':
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"),
+ (wallTime.getYearDay() + DAYSPERWEEK - wallTime.getWeekDay())
+ / DAYSPERWEEK);
+ return false;
+ case 'u':
+ int day = (wallTime.getWeekDay() == 0) ? DAYSPERWEEK : wallTime.getWeekDay();
+ outputFormatter.format("%d", day);
+ return false;
+ case 'V': /* ISO 8601 week number */
+ case 'G': /* ISO 8601 year (four digits) */
+ case 'g': /* ISO 8601 year (two digits) */
+ {
+ int year = wallTime.getYear();
+ int yday = wallTime.getYearDay();
+ int wday = wallTime.getWeekDay();
+ int w;
+ while (true) {
+ int len = isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
+ // What yday (-3 ... 3) does the ISO year begin on?
+ int bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3;
+ // What yday does the NEXT ISO year begin on?
+ int top = bot - (len % DAYSPERWEEK);
+ if (top < -3) {
+ top += DAYSPERWEEK;
+ }
+ top += len;
+ if (yday >= top) {
+ ++year;
+ w = 1;
+ break;
+ }
+ if (yday >= bot) {
+ w = 1 + ((yday - bot) / DAYSPERWEEK);
+ break;
+ }
+ --year;
+ yday += isLeap(year) ? DAYSPERLYEAR : DAYSPERNYEAR;
+ }
+ if (currentByteAsChar == 'V') {
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), w);
+ } else if (currentByteAsChar == 'g') {
+ outputYear(year, false, true, modifier);
+ } else {
+ outputYear(year, true, true, modifier);
+ }
+ return false;
+ }
+ case 'v':
+ formatInternal("%e-%b-%Y", wallTime, zoneInfo);
+ return false;
+ case 'W':
+ int n = (wallTime.getYearDay() + DAYSPERWEEK - (
+ wallTime.getWeekDay() != 0 ? (wallTime.getWeekDay() - 1)
+ : (DAYSPERWEEK - 1))) / DAYSPERWEEK;
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
+ return false;
+ case 'w':
+ outputFormatter.format("%d", wallTime.getWeekDay());
+ return false;
+ case 'X':
+ formatInternal(timeOnlyFormat, wallTime, zoneInfo);
+ return false;
+ case 'x':
+ formatInternal(dateOnlyFormat, wallTime, zoneInfo);
+ return false;
+ case 'y':
+ outputYear(wallTime.getYear(), false, true, modifier);
+ return false;
+ case 'Y':
+ outputYear(wallTime.getYear(), true, true, modifier);
+ return false;
+ case 'Z':
+ if (wallTime.getIsDst() < 0) {
+ return false;
+ }
+ boolean isDst = wallTime.getIsDst() != 0;
+ modifyAndAppend(zoneInfo.getDisplayName(isDst, TimeZone.SHORT), modifier);
+ return false;
+ case 'z': {
+ if (wallTime.getIsDst() < 0) {
+ return false;
+ }
+ int diff = wallTime.getGmtOffset();
+ String sign;
+ if (diff < 0) {
+ sign = "-";
+ diff = -diff;
+ } else {
+ sign = "+";
+ }
+ modifyAndAppend(sign, modifier);
+ diff /= SECSPERMIN;
+ diff = (diff / MINSPERHOUR) * 100 + (diff % MINSPERHOUR);
+ outputFormatter.format(getFormat(modifier, "%04d", "%4d", "%d", "%04d"), diff);
+ return false;
+ }
+ case '+':
+ formatInternal("%a %b %e %H:%M:%S %Z %Y", wallTime, zoneInfo);
+ return false;
+ case '%':
+ // If conversion char is undefined, behavior is undefined. Print out the
+ // character itself.
+ default:
+ return true;
+ }
+ }
+ return true;
+ }
+
+ private void modifyAndAppend(CharSequence str, int modifier) {
+ switch (modifier) {
+ case FORCE_LOWER_CASE:
+ for (int i = 0; i < str.length(); i++) {
+ outputBuilder.append(brokenToLower(str.charAt(i)));
+ }
+ break;
+ case '^':
+ for (int i = 0; i < str.length(); i++) {
+ outputBuilder.append(brokenToUpper(str.charAt(i)));
+ }
+ break;
+ case '#':
+ for (int i = 0; i < str.length(); i++) {
+ char c = str.charAt(i);
+ if (brokenIsUpper(c)) {
+ c = brokenToLower(c);
+ } else if (brokenIsLower(c)) {
+ c = brokenToUpper(c);
+ }
+ outputBuilder.append(c);
+ }
+ break;
+ default:
+ outputBuilder.append(str);
+
+ }
+ }
+
+ private void outputYear(int value, boolean outputTop, boolean outputBottom, int modifier) {
+ int lead;
+ int trail;
+
+ final int DIVISOR = 100;
+ trail = value % DIVISOR;
+ lead = value / DIVISOR + trail / DIVISOR;
+ trail %= DIVISOR;
+ if (trail < 0 && lead > 0) {
+ trail += DIVISOR;
+ --lead;
+ } else if (lead < 0 && trail > 0) {
+ trail -= DIVISOR;
+ ++lead;
+ }
+ if (outputTop) {
+ if (lead == 0 && trail < 0) {
+ modifyAndAppend("-0", modifier);
+ } else {
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), lead);
+ }
+ }
+ if (outputBottom) {
+ int n = ((trail < 0) ? -trail : trail);
+ outputFormatter.format(getFormat(modifier, "%02d", "%2d", "%d", "%02d"), n);
+ }
+ }
+
+ private static String getFormat(int modifier, String normal, String underscore, String dash,
+ String zero) {
+ switch (modifier) {
+ case '_':
+ return underscore;
+ case '-':
+ return dash;
+ case '0':
+ return zero;
+ }
+ return normal;
+ }
+
+ private static boolean isLeap(int year) {
+ return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
+ }
+
+ /**
+ * A broken implementation of {@link Character#isUpperCase(char)} that assumes ASCII in order to
+ * be compatible with the old native implementation.
+ */
+ private static boolean brokenIsUpper(char toCheck) {
+ return toCheck >= 'A' && toCheck <= 'Z';
+ }
+
+ /**
+ * A broken implementation of {@link Character#isLowerCase(char)} that assumes ASCII in order to
+ * be compatible with the old native implementation.
+ */
+ private static boolean brokenIsLower(char toCheck) {
+ return toCheck >= 'a' && toCheck <= 'z';
+ }
+
+ /**
+ * A broken implementation of {@link Character#toLowerCase(char)} that assumes ASCII in order to
+ * be compatible with the old native implementation.
+ */
+ private static char brokenToLower(char input) {
+ if (input >= 'A' && input <= 'Z') {
+ return (char) (input - 'A' + 'a');
+ }
+ return input;
+ }
+
+ /**
+ * A broken implementation of {@link Character#toUpperCase(char)} that assumes ASCII in order to
+ * be compatible with the old native implementation.
+ */
+ private static char brokenToUpper(char input) {
+ if (input >= 'a' && input <= 'z') {
+ return (char) (input - 'a' + 'A');
+ }
+ return input;
+ }
+
+ /**
+ * Safely convert a byte containing an ASCII character to a char, even for character codes
+ * > 127.
+ */
+ private static char convertToChar(byte b) {
+ return (char) (b & 0xFF);
+ }
+}