diff options
7 files changed, 894 insertions, 455 deletions
diff --git a/src/com/android/calendarcommon2/EventRecurrence.java b/src/com/android/calendarcommon2/EventRecurrence.java index df0c6eb..d54f2fe 100644 --- a/src/com/android/calendarcommon2/EventRecurrence.java +++ b/src/com/android/calendarcommon2/EventRecurrence.java @@ -18,7 +18,6 @@ package com.android.calendarcommon2; import android.text.TextUtils; import android.util.Log; -import android.util.TimeFormatException; import java.util.Calendar; import java.util.HashMap; @@ -475,7 +474,7 @@ public class EventRecurrence { EventRecurrence er = (EventRecurrence) obj; return (startDate == null ? - er.startDate == null : Time.compare(startDate, er.startDate) == 0) && + er.startDate == null : startDate.compareTo(er.startDate) == 0) && freq == er.freq && (until == null ? er.until == null : until.equals(er.until)) && count == er.count && @@ -739,7 +738,7 @@ public class EventRecurrence { // Parse the time to validate it. The result isn't retained. Time until = new Time(); until.parse(value); - } catch (TimeFormatException tfe) { + } catch (IllegalArgumentException iae) { throw new InvalidFormatException("Invalid UNTIL value: " + value); } } diff --git a/src/com/android/calendarcommon2/RecurrenceProcessor.java b/src/com/android/calendarcommon2/RecurrenceProcessor.java index 2707eec..24decce 100644 --- a/src/com/android/calendarcommon2/RecurrenceProcessor.java +++ b/src/com/android/calendarcommon2/RecurrenceProcessor.java @@ -92,7 +92,7 @@ public class RecurrenceProcessor } else if (rrule.until != null) { // according to RFC 2445, until must be in UTC. mIterator.parse(rrule.until); - long untilTime = mIterator.toMillis(false /* use isDst */); + long untilTime = mIterator.toMillis(); if (untilTime > lastTime) { lastTime = untilTime; } @@ -128,9 +128,8 @@ public class RecurrenceProcessor // The expansion might not contain any dates if the exrule or // exdates cancel all the generated dates. long[] dates = expand(dtstart, recur, - dtstart.toMillis(false /* use isDst */) /* range start */, - (maxtime != null) ? - maxtime.toMillis(false /* use isDst */) : -1 /* range end */); + dtstart.toMillis() /* range start */, + (maxtime != null) ? maxtime.toMillis() : -1 /* range end */); // The expansion might not contain any dates if exrule or exdates // cancel all the generated dates. @@ -200,7 +199,7 @@ public class RecurrenceProcessor // BYMONTH if (r.bymonthCount > 0) { found = listContains(r.bymonth, r.bymonthCount, - iterator.month + 1); + iterator.getMonth() + 1); if (!found) { return 1; } @@ -222,7 +221,7 @@ public class RecurrenceProcessor // BYYEARDAY if (r.byyeardayCount > 0) { found = listContains(r.byyearday, r.byyeardayCount, - iterator.yearDay, iterator.getActualMaximum(Time.YEAR_DAY)); + iterator.getYearDay(), iterator.getActualMaximum(Time.YEAR_DAY)); if (!found) { return 3; } @@ -230,7 +229,7 @@ public class RecurrenceProcessor // BYMONTHDAY if (r.bymonthdayCount > 0 ) { found = listContains(r.bymonthday, r.bymonthdayCount, - iterator.monthDay, + iterator.getDay(), iterator.getActualMaximum(Time.MONTH_DAY)); if (!found) { return 4; @@ -242,7 +241,7 @@ byday: if (r.bydayCount > 0) { int a[] = r.byday; int N = r.bydayCount; - int v = EventRecurrence.timeDay2Day(iterator.weekDay); + int v = EventRecurrence.timeDay2Day(iterator.getWeekDay()); for (int i=0; i<N; i++) { if (a[i] == v) { break byday; @@ -254,7 +253,7 @@ byday: if (EventRecurrence.HOURLY >= freq) { // BYHOUR found = listContains(r.byhour, r.byhourCount, - iterator.hour, + iterator.getHour(), iterator.getActualMaximum(Time.HOUR)); if (!found) { return 6; @@ -263,7 +262,7 @@ byday: if (EventRecurrence.MINUTELY >= freq) { // BYMINUTE found = listContains(r.byminute, r.byminuteCount, - iterator.minute, + iterator.getMinute(), iterator.getActualMaximum(Time.MINUTE)); if (!found) { return 7; @@ -272,7 +271,7 @@ byday: if (EventRecurrence.SECONDLY >= freq) { // BYSECOND found = listContains(r.bysecond, r.bysecondCount, - iterator.second, + iterator.getSecond(), iterator.getActualMaximum(Time.SECOND)); if (!found) { return 8; @@ -325,7 +324,7 @@ bysetpos: * (day of the month - 1) mod 7, and then make sure it's positive. We can simplify * that with some algebra. */ - int dotw = (instance.weekDay - instance.monthDay + 36) % 7; + int dotw = (instance.getWeekDay() - instance.getDay() + 36) % 7; /* * The byday[] values are specified as bits, so we can just OR them all @@ -367,14 +366,14 @@ bysetpos: if (index > daySetLength) { continue; // out of range } - if (daySet[index-1] == instance.monthDay) { + if (daySet[index-1] == instance.getDay()) { return true; } } else if (index < 0) { if (daySetLength + index < 0) { continue; // out of range } - if (daySet[daySetLength + index] == instance.monthDay) { + if (daySet[daySetLength + index] == instance.getDay()) { return true; } } else { @@ -428,29 +427,29 @@ bysetpos: boolean get(Time iterator, int day) { - int realYear = iterator.year; - int realMonth = iterator.month; + int realYear = iterator.getYear(); + int realMonth = iterator.getMonth(); Time t = null; if (SPEW) { Log.i(TAG, "get called with iterator=" + iterator - + " " + iterator.month - + "/" + iterator.monthDay - + "/" + iterator.year + " day=" + day); + + " " + iterator.getMonth() + + "/" + iterator.getDay() + + "/" + iterator.getYear() + " day=" + day); } if (day < 1 || day > 28) { // if might be past the end of the month, we need to normalize it t = mTime; t.set(day, realMonth, realYear); unsafeNormalize(t); - realYear = t.year; - realMonth = t.month; - day = t.monthDay; + realYear = t.getYear(); + realMonth = t.getMonth(); + day = t.getDay(); if (SPEW) { - Log.i(TAG, "normalized t=" + t + " " + t.month - + "/" + t.monthDay - + "/" + t.year); + Log.i(TAG, "normalized t=" + t + " " + t.getMonth() + + "/" + t.getDay() + + "/" + t.getYear()); } } @@ -465,9 +464,9 @@ bysetpos: t.set(day, realMonth, realYear); unsafeNormalize(t); if (SPEW) { - Log.i(TAG, "set t=" + t + " " + t.month - + "/" + t.monthDay - + "/" + t.year + Log.i(TAG, "set t=" + t + " " + t.getMonth() + + "/" + t.getDay() + + "/" + t.getYear() + " realMonth=" + realMonth + " mMonth=" + mMonth); } } @@ -506,11 +505,11 @@ bysetpos: count = r.bydayCount; if (count > 0) { // calculate the day of week for the first of this month (first) - j = generated.monthDay; + j = generated.getDay(); while (j >= 8) { j -= 7; } - first = generated.weekDay; + first = generated.getWeekDay(); if (first >= j) { first = first - j + 1; } else { @@ -630,13 +629,13 @@ bysetpos: * UTC milliseconds; use -1 for the entire range. * @return an array of dates, each date is in UTC milliseconds * @throws DateException - * @throws android.util.TimeFormatException if recur cannot be parsed + * @throws IllegalArgumentException if recur cannot be parsed */ public long[] expand(Time dtstart, RecurrenceSet recur, long rangeStartMillis, long rangeEndMillis) throws DateException { - String timezone = dtstart.timezone; + String timezone = dtstart.getTimezone(); mIterator.clear(timezone); mGenerated.clear(timezone); @@ -702,7 +701,7 @@ bysetpos: int i = 0; for (Long val: dtSet) { setTimeFromLongValue(mIterator, val); - dates[i++] = mIterator.toMillis(true /* ignore isDst */); + dates[i++] = mIterator.toMillis(); } return dates; } @@ -727,7 +726,7 @@ bysetpos: * @param add Whether or not we should add to out, or remove from out. * @param out the TreeSet you'd like to fill with the events * @throws DateException - * @throws android.util.TimeFormatException if r cannot be parsed. + * @throws IllegalArgumentException if r cannot be parsed. */ public void expand(Time dtstart, EventRecurrence r, @@ -826,7 +825,7 @@ bysetpos: // we'll skip months if it's greater than 28. // XXX Do we generate days for MONTHLY w/ BYHOUR? If so, // we need to do this then too. - iterator.monthDay = 1; + iterator.setDay(1); } } @@ -846,7 +845,7 @@ bysetpos: // We need the "until" year/month/day values to be in the same // timezone as all the generated dates so that we can compare them // using the values returned by normDateTimeComparisonValue(). - until.switchTimezone(dtstart.timezone); + until.switchTimezone(dtstart.getTimezone()); untilDateValue = normDateTimeComparisonValue(until); } else { untilDateValue = Long.MAX_VALUE; @@ -875,17 +874,17 @@ bysetpos: unsafeNormalize(iterator); - int iteratorYear = iterator.year; - int iteratorMonth = iterator.month + 1; - int iteratorDay = iterator.monthDay; - int iteratorHour = iterator.hour; - int iteratorMinute = iterator.minute; - int iteratorSecond = iterator.second; + int iteratorYear = iterator.getYear(); + int iteratorMonth = iterator.getMonth() + 1; + int iteratorDay = iterator.getDay(); + int iteratorHour = iterator.getHour(); + int iteratorMinute = iterator.getMinute(); + int iteratorSecond = iterator.getSecond(); // year is never expanded -- there is no BYYEAR generated.set(iterator); - if (SPEW) Log.i(TAG, "year=" + generated.year); + if (SPEW) Log.i(TAG, "year=" + generated.getYear()); do { // month int month = usebymonth @@ -922,9 +921,9 @@ bysetpos: * Thursday. If weeks started on Mondays, we would only * need to move back (2 - 1 + 7) % 7 = 1 day. */ - int weekStartAdj = (iterator.weekDay - + int weekStartAdj = (iterator.getWeekDay() - EventRecurrence.day2TimeDay(r.wkst) + 7) % 7; - dayIndex = iterator.monthDay - weekStartAdj; + dayIndex = iterator.getDay() - weekStartAdj; lastDayToExamine = dayIndex + 6; } else { lastDayToExamine = generated @@ -1064,35 +1063,21 @@ bysetpos: // We don't want to "generate" dates with the iterator. // XXX: We do this for days, because there is a varying number of days // per month - int oldDay = iterator.monthDay; + int oldDay = iterator.getDay(); generated.set(iterator); // just using generated as a temporary. int n = 1; while (true) { int value = freqAmount * n; switch (freqField) { case Time.SECOND: - iterator.second += value; - break; case Time.MINUTE: - iterator.minute += value; - break; case Time.HOUR: - iterator.hour += value; - break; case Time.MONTH_DAY: - iterator.monthDay += value; - break; case Time.MONTH: - iterator.month += value; - break; case Time.YEAR: - iterator.year += value; - break; case Time.WEEK_DAY: - iterator.monthDay += value; - break; case Time.YEAR_DAY: - iterator.monthDay += value; + iterator.add(freqField, value); break; default: throw new RuntimeException("bad field=" + freqField); @@ -1102,7 +1087,7 @@ bysetpos: if (freqField != Time.YEAR && freqField != Time.MONTH) { break; } - if (iterator.monthDay == oldDay) { + if (iterator.getDay() == oldDay) { break; } n++; @@ -1135,12 +1120,12 @@ bysetpos: * This method does not modify the fields isDst, or gmtOff. */ static void unsafeNormalize(Time date) { - int second = date.second; - int minute = date.minute; - int hour = date.hour; - int monthDay = date.monthDay; - int month = date.month; - int year = date.year; + int second = date.getSecond(); + int minute = date.getMinute(); + int hour = date.getHour(); + int monthDay = date.getDay(); + int month = date.getMonth(); + int year = date.getYear(); int addMinutes = ((second < 0) ? (second - 59) : second) / 60; second -= addMinutes * 60; @@ -1201,14 +1186,14 @@ bysetpos: // At this point, monthDay <= the length of the current month and is // in the range [1,31]. - date.second = second; - date.minute = minute; - date.hour = hour; - date.monthDay = monthDay; - date.month = month; - date.year = year; - date.weekDay = weekDay(year, month, monthDay); - date.yearDay = yearDay(year, month, monthDay); + date.setSecond(second); + date.setMinute(minute); + date.setHour(hour); + date.setDay(monthDay); + date.setMonth(month); + date.setYear(year); + date.setWeekDay(weekDay(year, month, monthDay)); + date.setYearDay(yearDay(year, month, monthDay)); } /** @@ -1299,17 +1284,17 @@ bysetpos: private static final long normDateTimeComparisonValue(Time normalized) { // 37 bits for the year, 4 bits for the month, 5 bits for the monthDay, // 5 bits for the hour, 6 bits for the minute, 6 bits for the second. - return ((long)normalized.year << 26) + (normalized.month << 22) - + (normalized.monthDay << 17) + (normalized.hour << 12) - + (normalized.minute << 6) + normalized.second; + return ((long)normalized.getYear() << 26) + (normalized.getMonth() << 22) + + (normalized.getDay() << 17) + (normalized.getHour() << 12) + + (normalized.getMinute() << 6) + normalized.getSecond(); } private static final void setTimeFromLongValue(Time date, long val) { - date.year = (int) (val >> 26); - date.month = (int) (val >> 22) & 0xf; - date.monthDay = (int) (val >> 17) & 0x1f; - date.hour = (int) (val >> 12) & 0x1f; - date.minute = (int) (val >> 6) & 0x3f; - date.second = (int) (val & 0x3f); + date.setYear((int) (val >> 26)); + date.setMonth((int) (val >> 22) & 0xf); + date.setDay((int) (val >> 17) & 0x1f); + date.setHour((int) (val >> 12) & 0x1f); + date.setMinute((int) (val >> 6) & 0x3f); + date.setSecond((int) (val & 0x3f)); } } diff --git a/src/com/android/calendarcommon2/RecurrenceSet.java b/src/com/android/calendarcommon2/RecurrenceSet.java index 6c153db..e42c0e9 100644 --- a/src/com/android/calendarcommon2/RecurrenceSet.java +++ b/src/com/android/calendarcommon2/RecurrenceSet.java @@ -21,7 +21,6 @@ import android.database.Cursor; import android.provider.CalendarContract; import android.text.TextUtils; import android.util.Log; -import android.util.TimeFormatException; import java.util.ArrayList; import java.util.List; @@ -161,14 +160,14 @@ public class RecurrenceSet { // The timezone is updated to UTC if the time string specified 'Z'. try { time.parse(rawDates[i]); - } catch (TimeFormatException e) { + } catch (IllegalArgumentException e) { throw new EventRecurrence.InvalidFormatException( - "TimeFormatException thrown when parsing time " + rawDates[i] + "IllegalArgumentException thrown when parsing time " + rawDates[i] + " in recurrence " + recurrence); } - dates[i] = time.toMillis(false /* use isDst */); - time.timezone = tz; + dates[i] = time.toMillis(); + time.setTimezone(tz); } return dates; } @@ -195,8 +194,9 @@ public class RecurrenceSet { // NOTE: the timezone may be null, if this is a floating time. String tzid = tzidParam == null ? null : tzidParam.value; Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid); - boolean inUtc = start.parse(dtstart); - boolean allDay = start.allDay; + start.parse(dtstart); + boolean inUtc = dtstart.length() == 16 && dtstart.charAt(15) == 'Z'; + boolean allDay = start.isAllDay(); // We force TimeZone to UTC for "all day recurring events" as the server is sending no // TimeZone in DTSTART for them @@ -223,9 +223,9 @@ public class RecurrenceSet { } if (allDay) { - start.timezone = Time.TIMEZONE_UTC; + start.setTimezone(Time.TIMEZONE_UTC); } - long millis = start.toMillis(false /* use isDst */); + long millis = start.toMillis(); values.put(CalendarContract.Events.DTSTART, millis); if (millis == -1) { if (false) { @@ -242,7 +242,7 @@ public class RecurrenceSet { values.put(CalendarContract.Events.DURATION, duration); values.put(CalendarContract.Events.ALL_DAY, allDay ? 1 : 0); return true; - } catch (TimeFormatException e) { + } catch (IllegalArgumentException e) { // Something is wrong with the format of this event Log.i(TAG,"Failed to parse event: " + component.toString()); return false; @@ -300,10 +300,10 @@ public class RecurrenceSet { // TODO: android.pim.Time really should take care of this for us. if (allDay) { dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); - dtstartTime.allDay = true; - dtstartTime.hour = 0; - dtstartTime.minute = 0; - dtstartTime.second = 0; + dtstartTime.setAllDay(true); + dtstartTime.setHour(0); + dtstartTime.setMinute(0); + dtstartTime.setSecond(0); } dtstartProp.setValue(dtstartTime.format2445()); @@ -359,10 +359,10 @@ public static boolean populateComponent(ContentValues values, // TODO: android.pim.Time really should take care of this for us. if (allDay) { dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); - dtstartTime.allDay = true; - dtstartTime.hour = 0; - dtstartTime.minute = 0; - dtstartTime.second = 0; + dtstartTime.setAllDay(true); + dtstartTime.setHour(0); + dtstartTime.setMinute(0); + dtstartTime.setSecond(0); } dtstartProp.setValue(dtstartTime.format2445()); @@ -479,14 +479,13 @@ public static boolean populateComponent(ContentValues values, ICalendar.Parameter endTzidParameter = dtendProperty.getFirstParameter("TZID"); String endTzid = (endTzidParameter == null) - ? start.timezone : endTzidParameter.value; + ? start.getTimezone() : endTzidParameter.value; Time end = new Time(endTzid); end.parse(dtendProperty.getValue()); - long durationMillis = end.toMillis(false /* use isDst */) - - start.toMillis(false /* use isDst */); + long durationMillis = end.toMillis() - start.toMillis(); long durationSeconds = (durationMillis / 1000); - if (start.allDay && (durationSeconds % 86400) == 0) { + if (start.isAllDay() && (durationSeconds % 86400) == 0) { return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S } else { return "P" + durationSeconds + "S"; diff --git a/src/com/android/calendarcommon2/Time.java b/src/com/android/calendarcommon2/Time.java index 08189cc..f0af248 100644 --- a/src/com/android/calendarcommon2/Time.java +++ b/src/com/android/calendarcommon2/Time.java @@ -15,6 +15,12 @@ */ package com.android.calendarcommon2; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + /** * Helper class to make migration out of android.text.format.Time smoother. */ @@ -22,124 +28,155 @@ public class Time { public static final String TIMEZONE_UTC = "UTC"; + private static final int EPOCH_JULIAN_DAY = 2440588; + private static final long HOUR_IN_MILLIS = 60 * 60 * 1000; + private static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS; + + private static final String FORMAT_ALL_DAY_PATTERN = "yyyyMMdd"; + private static final String FORMAT_TIME_PATTERN = "yyyyMMdd'T'HHmmss"; + private static final String FORMAT_TIME_UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'"; + private static final String FORMAT_LOG_TIME_PATTERN = "EEE, MMM dd, yyyy hh:mm a"; + /* * Define symbolic constants for accessing the fields in this class. Used in * getActualMaximum(). */ - public static final int SECOND = android.text.format.Time.SECOND; - public static final int MINUTE = android.text.format.Time.MINUTE; - public static final int HOUR = android.text.format.Time.HOUR; - public static final int MONTH_DAY = android.text.format.Time.MONTH_DAY; - public static final int MONTH = android.text.format.Time.MONTH; - public static final int YEAR = android.text.format.Time.YEAR; - public static final int WEEK_DAY = android.text.format.Time.WEEK_DAY; - public static final int YEAR_DAY = android.text.format.Time.YEAR_DAY; - public static final int WEEK_NUM = android.text.format.Time.WEEK_NUM; - - public static final int SUNDAY = android.text.format.Time.SUNDAY; - public static final int MONDAY = android.text.format.Time.MONDAY; - public static final int TUESDAY = android.text.format.Time.TUESDAY; - public static final int WEDNESDAY = android.text.format.Time.WEDNESDAY; - public static final int THURSDAY = android.text.format.Time.THURSDAY; - public static final int FRIDAY = android.text.format.Time.FRIDAY; - public static final int SATURDAY = android.text.format.Time.SATURDAY; - - private final android.text.format.Time mInstance; - - public int year; - public int month; - public int hour; - public int minute; - public int second; - - public int yearDay; - public int monthDay; - public int weekDay; - - public String timezone; - public int isDst; - public long gmtoff; - public boolean allDay; + 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; + + private final GregorianCalendar mCalendar; + + private int year; + private int month; + private int monthDay; + private int hour; + private int minute; + private int second; + + private int yearDay; + private int weekDay; + + private String timezone; + private boolean allDay; + + /** + * Enabling this flag will apply appropriate dst transition logic when calling either + * {@code toMillis()} or {@code normalize()} and their respective *ApplyDst() equivalents. <br> + * When this flag is enabled, the following calls would be considered equivalent: + * <ul> + * <li>{@code a.t.f.Time#normalize(true)} and {@code #normalize()}</li> + * <li>{@code a.t.f.Time#toMillis(true)} and {@code #toMillis()}</li> + * <li>{@code a.t.f.Time#normalize(false)} and {@code #normalizeApplyDst()}</li> + * <li>{@code a.t.f.Time#toMillis(false)} and {@code #toMillisApplyDst()}</li> + * </ul> + * When the flag is disabled, both {@code toMillis()} and {@code normalize()} will ignore any + * dst transitions unless minutes or hours were added to the time (the default behavior of the + * a.t.f.Time class). <br> + * + * NOTE: currently, this flag is disabled because there are no direct manipulations of the day, + * hour, or minute fields. All of the accesses are correctly done via setters and they rely on + * a private normalize call in their respective classes to achieve their expected behavior. + * Additionally, using any of the {@code #set()} methods or {@code #parse()} will result in + * normalizing by ignoring DST, which is what the default behavior is for the a.t.f.Time class. + */ + static final boolean APPLY_DST_CHANGE_LOGIC = false; + private int mDstChangedByField = -1; public Time() { - mInstance = new android.text.format.Time(); - readFields(); + this(TimeZone.getDefault().getID()); } public Time(String timezone) { - mInstance = new android.text.format.Time(timezone); - readFields(); + if (timezone == null) { + throw new NullPointerException("timezone cannot be null."); + } + this.timezone = timezone; + // Although the process's default locale is used here, #clear() will explicitly set the + // first day of the week to MONDAY to match with the expected a.t.f.Time implementation. + mCalendar = new GregorianCalendar(getTimeZone(), Locale.getDefault()); + clear(this.timezone); } - private void readFields() { - year = mInstance.year; - month = mInstance.month; - hour = mInstance.hour; - minute = mInstance.minute; - second = mInstance.second; - - yearDay = mInstance.yearDay; - monthDay = mInstance.monthDay; - weekDay = mInstance.weekDay; - - timezone = mInstance.timezone; - isDst = mInstance.isDst; - gmtoff = mInstance.gmtoff; - allDay = mInstance.allDay; + private void readFieldsFromCalendar() { + year = mCalendar.get(Calendar.YEAR); + month = mCalendar.get(Calendar.MONTH); + monthDay = mCalendar.get(Calendar.DAY_OF_MONTH); + hour = mCalendar.get(Calendar.HOUR_OF_DAY); + minute = mCalendar.get(Calendar.MINUTE); + second = mCalendar.get(Calendar.SECOND); } - private void writeFields() { - mInstance.year = year; - mInstance.month = month; - mInstance.hour = hour; - mInstance.minute = minute; - mInstance.second = second; + private void writeFieldsToCalendar() { + clearCalendar(); + mCalendar.set(year, month, monthDay, hour, minute, second); + mCalendar.set(Calendar.MILLISECOND, 0); + } - mInstance.yearDay = yearDay; - mInstance.monthDay = monthDay; - mInstance.weekDay = weekDay; + private boolean isInDst() { + return mCalendar.getTimeZone().inDaylightTime(mCalendar.getTime()); + } - mInstance.timezone = timezone; - mInstance.isDst = isDst; - mInstance.gmtoff = gmtoff; - mInstance.allDay = allDay; + public void add(int field, int amount) { + final boolean wasDstBefore = isInDst(); + mCalendar.add(getCalendarField(field), amount); + if (APPLY_DST_CHANGE_LOGIC && wasDstBefore != isInDst() + && (field == MONTH_DAY || field == HOUR || field == MINUTE)) { + mDstChangedByField = field; + } } public void set(long millis) { - writeFields(); - mInstance.set(millis); - readFields(); + clearCalendar(); + mCalendar.setTimeInMillis(millis); + readFieldsFromCalendar(); } public void set(Time other) { - other.writeFields(); - mInstance.set(other.mInstance); - readFields(); + clearCalendar(); + mCalendar.setTimeZone(other.getTimeZone()); + mCalendar.setTimeInMillis(other.mCalendar.getTimeInMillis()); + readFieldsFromCalendar(); } public void set(int day, int month, int year) { - writeFields(); - mInstance.set(day, month, year); - readFields(); + clearCalendar(); + mCalendar.set(year, month, day); + readFieldsFromCalendar(); } public void set(int second, int minute, int hour, int day, int month, int year) { - writeFields(); - mInstance.set(second, minute, hour, day, month, year); - readFields(); - } - - public void setToNow() { - writeFields(); - mInstance.setToNow(); - readFields(); + clearCalendar(); + mCalendar.set(year, month, day, hour, minute, second); + readFieldsFromCalendar(); } public long setJulianDay(int julianDay) { - writeFields(); - final long ms = mInstance.setJulianDay(julianDay); - readFields(); - return ms; + long millis = (julianDay - EPOCH_JULIAN_DAY) * DAY_IN_MILLIS; + mCalendar.setTimeInMillis(millis); + readFieldsFromCalendar(); + + // adjust day approximation, set the time to 12am, and re-normalize + monthDay += julianDay - getJulianDay(millis, getGmtOffset()); + hour = 0; + minute = 0; + second = 0; + writeFieldsToCalendar(); + return normalize(); } public static int getJulianDay(long begin, long gmtOff) { @@ -147,72 +184,349 @@ public class Time { } public int getWeekNumber() { - writeFields(); - final int num = mInstance.getWeekNumber(); - readFields(); - return num; + return mCalendar.get(Calendar.WEEK_OF_YEAR); + } + + private int getCalendarField(int field) { + switch (field) { + case SECOND: return Calendar.SECOND; + case MINUTE: return Calendar.MINUTE; + case HOUR: return Calendar.HOUR_OF_DAY; + case MONTH_DAY: return Calendar.DAY_OF_MONTH; + case MONTH: return Calendar.MONTH; + case YEAR: return Calendar.YEAR; + case WEEK_DAY: return Calendar.DAY_OF_WEEK; + case YEAR_DAY: return Calendar.DAY_OF_YEAR; + case WEEK_NUM: return Calendar.WEEK_OF_YEAR; + default: + throw new RuntimeException("bad field=" + field); + } } public int getActualMaximum(int field) { - writeFields(); - return mInstance.getActualMaximum(field); + return mCalendar.getActualMaximum(getCalendarField(field)); } public void switchTimezone(String timezone) { - writeFields(); - mInstance.switchTimezone(timezone); - readFields(); + long msBefore = mCalendar.getTimeInMillis(); + mCalendar.setTimeZone(TimeZone.getTimeZone(timezone)); + mCalendar.setTimeInMillis(msBefore); + mDstChangedByField = -1; + readFieldsFromCalendar(); } - public long normalize(boolean ignoreDst) { - writeFields(); - final long ms = mInstance.normalize(ignoreDst); - readFields(); + /** + * @param apply whether to apply dst logic on the ms or not; if apply is true, it is equivalent + * to calling the normalize or toMillis APIs in a.t.f.Time with ignoreDst=false + */ + private long getDstAdjustedMillis(boolean apply, long ms) { + if (APPLY_DST_CHANGE_LOGIC) { + if (apply && mDstChangedByField == MONTH_DAY) { + return isInDst() ? (ms + HOUR_IN_MILLIS) : (ms - HOUR_IN_MILLIS); + } else if (!apply && (mDstChangedByField == HOUR || mDstChangedByField == MINUTE)) { + return isInDst() ? (ms - HOUR_IN_MILLIS) : (ms + HOUR_IN_MILLIS); + } + } return ms; } - public boolean parse(String time) { - writeFields(); - boolean success = mInstance.parse(time); - readFields(); - return success; + private long normalizeInternal() { + final long ms = mCalendar.getTimeInMillis(); + readFieldsFromCalendar(); + return ms; } - public boolean parse3339(String time) { - writeFields(); - boolean success = mInstance.parse3339(time); - readFields(); - return success; + public long normalize() { + return getDstAdjustedMillis(false, normalizeInternal()); } - public String format(String format) { - writeFields(); - return (new android.text.format.Time(mInstance)).format(format); + long normalizeApplyDst() { + return getDstAdjustedMillis(true, normalizeInternal()); + } + + public void parse(String time) { + if (time == null) { + throw new NullPointerException("time string is null"); + } + parseInternal(time); + writeFieldsToCalendar(); } public String format2445() { - writeFields(); - return (new android.text.format.Time(mInstance)).format2445(); + writeFieldsToCalendar(); + final SimpleDateFormat sdf = new SimpleDateFormat( + allDay ? FORMAT_ALL_DAY_PATTERN + : (TIMEZONE_UTC.equals(getTimezone()) ? FORMAT_TIME_UTC_PATTERN + : FORMAT_TIME_PATTERN)); + sdf.setTimeZone(getTimeZone()); + return sdf.format(mCalendar.getTime()); } - public String format3339(boolean allDay) { - writeFields(); - return (new android.text.format.Time(mInstance)).format3339(allDay); + public long toMillis() { + return getDstAdjustedMillis(false, mCalendar.getTimeInMillis()); + } + + long toMillisApplyDst() { + return getDstAdjustedMillis(true, mCalendar.getTimeInMillis()); } - public long toMillis(boolean ignoreDst) { - writeFields(); - return mInstance.toMillis(ignoreDst); + private TimeZone getTimeZone() { + return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault(); } - public static int compare(Time a, Time b) { - a.writeFields(); - b.writeFields(); - return android.text.format.Time.compare(a.mInstance, b.mInstance); + public int compareTo(Time other) { + return mCalendar.compareTo(other.mCalendar); + } + + private void clearCalendar() { + mDstChangedByField = -1; + mCalendar.clear(); + mCalendar.set(Calendar.HOUR_OF_DAY, 0); // HOUR_OF_DAY doesn't get reset with #clear + mCalendar.setTimeZone(getTimeZone()); + // set fields for week number computation according to ISO 8601. + mCalendar.setFirstDayOfWeek(Calendar.MONDAY); + mCalendar.setMinimalDaysInFirstWeek(4); } public void clear(String timezoneId) { - mInstance.clear(timezoneId); - readFields(); + clearCalendar(); + readFieldsFromCalendar(); + setTimezone(timezoneId); + } + + public int getYear() { + return mCalendar.get(Calendar.YEAR); + } + + public void setYear(int year) { + this.year = year; + mCalendar.set(Calendar.YEAR, year); + } + + public int getMonth() { + return mCalendar.get(Calendar.MONTH); + } + + public void setMonth(int month) { + this.month = month; + mCalendar.set(Calendar.MONTH, month); + } + + public int getDay() { + return mCalendar.get(Calendar.DAY_OF_MONTH); + } + + public void setDay(int day) { + this.monthDay = day; + mCalendar.set(Calendar.DAY_OF_MONTH, day); + } + + public int getHour() { + return mCalendar.get(Calendar.HOUR_OF_DAY); + } + + public void setHour(int hour) { + this.hour = hour; + mCalendar.set(Calendar.HOUR_OF_DAY, hour); + } + + public int getMinute() { + return mCalendar.get(Calendar.MINUTE); + } + + public void setMinute(int minute) { + this.minute = minute; + mCalendar.set(Calendar.MINUTE, minute); + } + + public int getSecond() { + return mCalendar.get(Calendar.SECOND); + } + + public void setSecond(int second) { + this.second = second; + mCalendar.set(Calendar.SECOND, second); + } + + public String getTimezone() { + return mCalendar.getTimeZone().getID(); + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + mCalendar.setTimeZone(getTimeZone()); + } + + public int getYearDay() { + // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + return mCalendar.get(Calendar.DAY_OF_YEAR) - 1; + } + + public void setYearDay(int yearDay) { + this.yearDay = yearDay; + // yearDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + mCalendar.set(Calendar.DAY_OF_YEAR, yearDay + 1); + } + + public int getWeekDay() { + // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + return mCalendar.get(Calendar.DAY_OF_WEEK) - 1; + } + + public void setWeekDay(int weekDay) { + this.weekDay = weekDay; + // weekDay in a.t.f.Time's implementation starts from 0, whereas Calendar's starts from 1. + mCalendar.set(Calendar.DAY_OF_WEEK, weekDay + 1); + } + + public boolean isAllDay() { + return allDay; + } + + public void setAllDay(boolean allDay) { + this.allDay = allDay; + } + + public long getGmtOffset() { + return mCalendar.getTimeZone().getOffset(mCalendar.getTimeInMillis()) / 1000; + } + + private void parseInternal(String s) { + int len = s.length(); + if (len < 8) { + throw new IllegalArgumentException("String is too short: \"" + s + + "\" Expected at least 8 characters."); + } else if (len > 8 && len < 15) { + throw new IllegalArgumentException("String is too short: \"" + s + + "\" If there are more than 8 characters there must be at least 15."); + } + + // 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) { + 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'); + timezone = TIMEZONE_UTC; + } + } else { + allDay = true; + hour = 0; + minute = 0; + second = 0; + } + + weekDay = 0; + yearDay = 0; + } + + private void checkChar(String s, int spos, char expected) { + final char c = s.charAt(spos); + if (c != expected) { + throw new IllegalArgumentException(String.format( + "Unexpected character 0x%02d at pos=%d. Expected 0x%02d (\'%c\').", + (int) c, spos, (int) expected, expected)); + } + } + + private int getChar(String s, int spos, int mul) { + final char c = s.charAt(spos); + if (Character.isDigit(c)) { + return Character.getNumericValue(c) * mul; + } else { + throw new IllegalArgumentException("Parse error at pos=" + spos); + } + } + + // NOTE: only used for outputting time to error logs + public String format() { + final SimpleDateFormat sdf = + new SimpleDateFormat(FORMAT_LOG_TIME_PATTERN, Locale.getDefault()); + return sdf.format(mCalendar.getTime()); + } + + // NOTE: only used in tests + public boolean parse3339(String time) { + android.text.format.Time tmp = generateInstance(); + boolean success = tmp.parse3339(time); + copyAndWriteInstance(tmp); + return success; + } + + // NOTE: only used in tests + public String format3339(boolean allDay) { + return generateInstance().format3339(allDay); + } + + private android.text.format.Time generateInstance() { + android.text.format.Time tmp = new android.text.format.Time(timezone); + tmp.set(second, minute, hour, monthDay, month, year); + + tmp.yearDay = yearDay; + tmp.weekDay = weekDay; + + tmp.timezone = timezone; + tmp.gmtoff = getGmtOffset(); + tmp.allDay = allDay; + tmp.set(mCalendar.getTimeInMillis()); + if (tmp.allDay && (tmp.hour != 0 || tmp.minute != 0 || tmp.second != 0)) { + // Time SDK expects hour, minute, second to be 0 if allDay is true + tmp.hour = 0; + tmp.minute = 0; + tmp.second = 0; + } + + return tmp; + } + + private void copyAndWriteInstance(android.text.format.Time time) { + year = time.year; + month = time.month; + monthDay = time.monthDay; + hour = time.hour; + minute = time.minute; + second = time.second; + + yearDay = time.yearDay; + weekDay = time.weekDay; + + timezone = time.timezone; + allDay = time.allDay; + + writeFieldsToCalendar(); } } diff --git a/tests/src/com/android/calendarcommon2/RRuleTest.java b/tests/src/com/android/calendarcommon2/RRuleTest.java index a473263..18217a3 100644 --- a/tests/src/com/android/calendarcommon2/RRuleTest.java +++ b/tests/src/com/android/calendarcommon2/RRuleTest.java @@ -114,8 +114,7 @@ public class RRuleTest extends TestCase { RecurrenceProcessor rp = new RecurrenceProcessor(); RecurrenceSet recur = new RecurrenceSet(rrule, rdate, exrule, exdate); - long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(false /* use isDst */), - rangeEnd.toMillis(false /* use isDst */)); + long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(), rangeEnd.toMillis()); if (METHOD_TRACE) { Debug.stopMethodTracing(); diff --git a/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java b/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java index 0687ec4..3cd9177 100644 --- a/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java +++ b/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java @@ -23,7 +23,6 @@ import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; import android.text.TextUtils; import android.util.Log; -import android.util.TimeFormatException; import junit.framework.TestCase; import java.util.TreeSet; @@ -105,8 +104,7 @@ public class RecurrenceProcessorTest extends TestCase { RecurrenceProcessor rp = new RecurrenceProcessor(); RecurrenceSet recur = new RecurrenceSet(rrule, rdate, exrule, exdate); - long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(false /* use isDst */), - rangeEnd.toMillis(false /* use isDst */)); + long[] out = rp.expand(dtstart, recur, rangeStart.toMillis(), rangeEnd.toMillis()); if (METHOD_TRACE) { Debug.stopMethodTracing(); @@ -149,12 +147,12 @@ public class RecurrenceProcessorTest extends TestCase { if (lastOccur != -1) { outCal.set(lastOccur); lastStr = outCal.format2445(); - lastMillis = outCal.toMillis(true /* ignore isDst */); + lastMillis = outCal.toMillis(); } if (last != null && last.length() > 0) { Time expectedLast = new Time(tz); expectedLast.parse(last); - expectedMillis = expectedLast.toMillis(true /* ignore isDst */); + expectedMillis = expectedLast.toMillis(); } if (lastMillis != expectedMillis) { if (SPEW) { @@ -597,7 +595,7 @@ public class RecurrenceProcessorTest extends TestCase { "20060219T100000" }, "20060220T020001"); fail("Bad UNTIL string failed to throw exception"); - } catch (TimeFormatException e) { + } catch (IllegalArgumentException e) { // expected } } @@ -2459,8 +2457,8 @@ public class RecurrenceProcessorTest extends TestCase { dtstart.parse("20010101T000000"); rangeStart.parse("20010101T000000"); rangeEnd.parse("20090101T000000"); - long rangeStartMillis = rangeStart.toMillis(false /* use isDst */); - long rangeEndMillis = rangeEnd.toMillis(false /* use isDst */); + long rangeStartMillis = rangeStart.toMillis(); + long rangeEndMillis = rangeEnd.toMillis(); long startTime = System.currentTimeMillis(); for (int iterations = 0; iterations < 5; iterations++) { @@ -2503,12 +2501,12 @@ public class RecurrenceProcessorTest extends TestCase { long startTime = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - date.month += 1; - date.monthDay += 100; - date.normalize(true); - date.month -= 1; - date.monthDay -= 100; - date.normalize(true); + date.add(Time.MONTH, 1); + date.add(Time.MONTH_DAY, 100); + date.normalize(); + date.add(Time.MONTH, -1); + date.add(Time.MONTH_DAY, -100); + date.normalize(); } long endTime = System.currentTimeMillis(); @@ -2520,11 +2518,11 @@ public class RecurrenceProcessorTest extends TestCase { date.parse("20090404T100000"); startTime = System.currentTimeMillis(); for (int i = 0; i < ITERATIONS; i++) { - date.month += 1; - date.monthDay += 100; + date.add(Time.MONTH, 1); + date.add(Time.MONTH_DAY, 100); RecurrenceProcessor.unsafeNormalize(date); - date.month -= 1; - date.monthDay -= 100; + date.add(Time.MONTH, -1); + date.add(Time.MONTH_DAY, -100); RecurrenceProcessor.unsafeNormalize(date); } diff --git a/tests/src/com/android/calendarcommon2/TimeTest.java b/tests/src/com/android/calendarcommon2/TimeTest.java index 600506a..df27c4f 100644 --- a/tests/src/com/android/calendarcommon2/TimeTest.java +++ b/tests/src/com/android/calendarcommon2/TimeTest.java @@ -29,9 +29,19 @@ import junit.framework.TestCase; public class TimeTest extends TestCase { @SmallTest + public void testNullTimezone() { + try { + Time t = new Time(null); + fail("expected a null timezone to throw an exception."); + } catch (NullPointerException npe) { + // expected. + } + } + + @SmallTest public void testTimezone() { Time t = new Time(Time.TIMEZONE_UTC); - assertEquals(Time.TIMEZONE_UTC, t.timezone); + assertEquals(Time.TIMEZONE_UTC, t.getTimezone()); } @SmallTest @@ -39,23 +49,40 @@ public class TimeTest extends TestCase { Time t = new Time(Time.TIMEZONE_UTC); String newTimezone = "America/Los_Angeles"; t.switchTimezone(newTimezone); - assertEquals(newTimezone, t.timezone); + assertEquals(newTimezone, t.getTimezone()); } @SmallTest public void testGetActualMaximum() { Time t = new Time(Time.TIMEZONE_UTC); - t.set(0, 0, 2020); + t.set(1, 0, 2020); assertEquals(59, t.getActualMaximum(Time.SECOND)); assertEquals(59, t.getActualMaximum(Time.MINUTE)); assertEquals(23, t.getActualMaximum(Time.HOUR)); assertEquals(31, t.getActualMaximum(Time.MONTH_DAY)); assertEquals(11, t.getActualMaximum(Time.MONTH)); - assertEquals(2037, t.getActualMaximum(Time.YEAR)); - assertEquals(6, t.getActualMaximum(Time.WEEK_DAY)); - assertEquals(365, t.getActualMaximum(Time.YEAR_DAY)); // 2020 is a leap year - t.set(0, 0, 2019); - assertEquals(364, t.getActualMaximum(Time.YEAR_DAY)); + assertEquals(7, t.getActualMaximum(Time.WEEK_DAY)); + assertEquals(366, t.getActualMaximum(Time.YEAR_DAY)); // 2020 is a leap year + t.set(1, 0, 2019); + assertEquals(365, t.getActualMaximum(Time.YEAR_DAY)); + } + + @SmallTest + public void testAdd() { + Time t = new Time(Time.TIMEZONE_UTC); + t.set(0, 0, 0, 1, 0, 2020); + t.add(Time.SECOND, 1); + assertEquals(1, t.getSecond()); + t.add(Time.MINUTE, 1); + assertEquals(1, t.getMinute()); + t.add(Time.HOUR, 1); + assertEquals(1, t.getHour()); + t.add(Time.MONTH_DAY, 1); + assertEquals(2, t.getDay()); + t.add(Time.MONTH, 1); + assertEquals(1, t.getMonth()); + t.add(Time.YEAR, 1); + assertEquals(2021, t.getYear()); } @SmallTest @@ -63,37 +90,40 @@ public class TimeTest extends TestCase { Time t = new Time(Time.TIMEZONE_UTC); t.clear(Time.TIMEZONE_UTC); - assertEquals(Time.TIMEZONE_UTC, t.timezone); - assertFalse(t.allDay); - assertEquals(0, t.second); - assertEquals(0, t.minute); - assertEquals(0, t.hour); - assertEquals(0, t.monthDay); - assertEquals(0, t.month); - assertEquals(0, t.year); - assertEquals(0, t.weekDay); - assertEquals(0, t.yearDay); - assertEquals(0, t.gmtoff); - assertEquals(-1, t.isDst); + assertEquals(Time.TIMEZONE_UTC, t.getTimezone()); + assertFalse(t.isAllDay()); + assertEquals(0, t.getSecond()); + assertEquals(0, t.getMinute()); + assertEquals(0, t.getHour()); + assertEquals(1, t.getDay()); // default for Calendar is 1 + assertEquals(0, t.getMonth()); + assertEquals(1970, t.getYear()); + assertEquals(4, t.getWeekDay()); // 1970 Jan 1 --> Thursday + assertEquals(0, t.getYearDay()); + assertEquals(0, t.getGmtOffset()); } @SmallTest public void testCompare() { Time a = new Time(Time.TIMEZONE_UTC); Time b = new Time("America/Los_Angeles"); - assertTrue(Time.compare(a, b) < 0); - } + assertTrue(a.compareTo(b) < 0); - @SmallTest - public void testFormat() { - Time t = new Time(Time.TIMEZONE_UTC); - assertEquals("19700101T000000", t.format("%Y%m%dT%H%M%S")); + Time c = new Time("Asia/Calcutta"); + assertTrue(a.compareTo(c) > 0); + + Time d = new Time(Time.TIMEZONE_UTC); + assertEquals(0, a.compareTo(d)); } @SmallTest public void testFormat2445() { - Time t = new Time(Time.TIMEZONE_UTC); + Time t = new Time(); + assertEquals("19700101T000000", t.format2445()); + t.setTimezone(Time.TIMEZONE_UTC); assertEquals("19700101T000000Z", t.format2445()); + t.setAllDay(true); + assertEquals("19700101", t.format2445()); } @SmallTest @@ -105,26 +135,63 @@ public class TimeTest extends TestCase { } @SmallTest - public void testMillis0() { + public void testToMillis() { Time t = new Time(Time.TIMEZONE_UTC); + t.set(1, 0, 0, 1, 0, 1970); + assertEquals(1000L, t.toMillis()); + t.set(0, 0, 0, 1, 1, 2020); - assertEquals(1580515200000L, t.toMillis(true)); + assertEquals(1580515200000L, t.toMillis()); t.set(1, 0, 0, 1, 1, 2020); - assertEquals(1580515201000L, t.toMillis(true)); + assertEquals(1580515201000L, t.toMillis()); + + t.set(1, 0, 2020); + assertEquals(1577836800000L, t.toMillis()); + t.set(1, 1, 2020); + assertEquals(1580515200000L, t.toMillis()); } @SmallTest - public void testMillis1() { + public void testToMillis_overflow() { Time t = new Time(Time.TIMEZONE_UTC); - t.set(1, 0, 0, 1, 0, 1970); - assertEquals(1000L, t.toMillis(true)); + t.set(32, 0, 2020); + assertEquals(1580515200000L, t.toMillis()); + assertEquals(1, t.getDay()); + assertEquals(1, t.getMonth()); } @SmallTest public void testParse() { Time t = new Time(Time.TIMEZONE_UTC); - assertTrue(t.parse("20201010T160000Z")); - assertFalse(t.parse("12345678T901234")); + t.parse("20201010T160000Z"); + assertEquals(2020, t.getYear()); + assertEquals(9, t.getMonth()); + assertEquals(10, t.getDay()); + assertEquals(16, t.getHour()); + assertEquals(0, t.getMinute()); + assertEquals(0, t.getSecond()); + + t.parse("20200220"); + assertEquals(2020, t.getYear()); + assertEquals(1, t.getMonth()); + assertEquals(20, t.getDay()); + assertEquals(0, t.getHour()); + assertEquals(0, t.getMinute()); + assertEquals(0, t.getSecond()); + + try { + t.parse("invalid"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + try { + t.parse("20201010Z160000"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } } @SmallTest @@ -132,51 +199,58 @@ public class TimeTest extends TestCase { Time t = new Time(Time.TIMEZONE_UTC); t.parse3339("1980-05-23"); - if (!t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23) { + if (!t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23) { fail("Did not parse all-day date correctly"); } t.parse3339("1980-05-23T09:50:50"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 9 - || t.minute != 50 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse timezone-offset-less date correctly"); } t.parse3339("1980-05-23T09:50:50Z"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 9 - || t.minute != 50 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse UTC date correctly"); } t.parse3339("1980-05-23T09:50:50.0Z"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 9 - || t.minute != 50 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse UTC date correctly"); } t.parse3339("1980-05-23T09:50:50.12Z"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 9 - || t.minute != 50 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse UTC date correctly"); } t.parse3339("1980-05-23T09:50:50.123Z"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 9 - || t.minute != 50 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 9 || t.getMinute() != 50 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse UTC date correctly"); } // the time should be normalized to UTC t.parse3339("1980-05-23T09:50:50-01:05"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 10 - || t.minute != 55 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 10 || t.getMinute() != 55 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse timezone-offset date correctly"); } // the time should be normalized to UTC t.parse3339("1980-05-23T09:50:50.123-01:05"); - if (t.allDay || t.year != 1980 || t.month != 4 || t.monthDay != 23 || t.hour != 10 - || t.minute != 55 || t.second != 50 || t.gmtoff != 0) { + if (t.isAllDay() || t.getYear() != 1980 || t.getMonth() != 4 || t.getDay() != 23 + || t.getHour() != 10 || t.getMinute() != 55 || t.getSecond() != 50 + || t.getGmtOffset() != 0) { fail("Did not parse timezone-offset date correctly"); } @@ -203,74 +277,93 @@ public class TimeTest extends TestCase { } @SmallTest - public void testSet0() { + public void testSet_millis() { Time t = new Time(Time.TIMEZONE_UTC); t.set(1000L); - assertEquals(1970, t.year); - assertEquals(1, t.second); + assertEquals(1970, t.getYear()); + assertEquals(1, t.getSecond()); t.set(2000L); - assertEquals(2, t.second); - assertEquals(0, t.minute); + assertEquals(2, t.getSecond()); + assertEquals(0, t.getMinute()); t.set(1000L * 60); - assertEquals(1, t.minute); - assertEquals(0, t.hour); + assertEquals(1, t.getMinute()); + assertEquals(0, t.getHour()); t.set(1000L * 60 * 60); - assertEquals(1, t.hour); - assertEquals(1, t.monthDay); + assertEquals(1, t.getHour()); + assertEquals(1, t.getDay()); t.set((1000L * 60 * 60 * 24) + 1000L); - assertEquals(2, t.monthDay); - assertEquals(1970, t.year); + assertEquals(2, t.getDay()); + assertEquals(1970, t.getYear()); } @SmallTest - public void testSet1() { + public void testSet_dayMonthYear() { Time t = new Time(Time.TIMEZONE_UTC); t.set(1, 2, 2021); - assertEquals(1, t.monthDay); - assertEquals(2, t.month); - assertEquals(2021, t.year); + assertEquals(1, t.getDay()); + assertEquals(2, t.getMonth()); + assertEquals(2021, t.getYear()); } @SmallTest - public void testSet2() { + public void testSet_secondMinuteHour() { Time t = new Time(Time.TIMEZONE_UTC); t.set(1, 2, 3, 4, 5, 2021); - assertEquals(1, t.second); - assertEquals(2, t.minute); - assertEquals(3, t.hour); - assertEquals(4, t.monthDay); - assertEquals(5, t.month); - assertEquals(2021, t.year); + assertEquals(1, t.getSecond()); + assertEquals(2, t.getMinute()); + assertEquals(3, t.getHour()); + assertEquals(4, t.getDay()); + assertEquals(5, t.getMonth()); + assertEquals(2021, t.getYear()); + } + + @SmallTest + public void testSet_overflow() { + // Jan 32nd --> Feb 1st + Time t = new Time(Time.TIMEZONE_UTC); + t.set(32, 0, 2020); + assertEquals(1, t.getDay()); + assertEquals(1, t.getMonth()); + assertEquals(2020, t.getYear()); + + t = new Time(Time.TIMEZONE_UTC); + t.set(5, 10, 15, 32, 0, 2020); + assertEquals(5, t.getSecond()); + assertEquals(10, t.getMinute()); + assertEquals(15, t.getHour()); + assertEquals(1, t.getDay()); + assertEquals(1, t.getMonth()); + assertEquals(2020, t.getYear()); } @SmallTest - public void testSet3() { + public void testSet_other() { Time t = new Time(Time.TIMEZONE_UTC); t.set(1, 2, 3, 4, 5, 2021); Time t2 = new Time(); t2.set(t); - assertEquals(Time.TIMEZONE_UTC, t2.timezone); - assertEquals(1, t2.second); - assertEquals(2, t2.minute); - assertEquals(3, t2.hour); - assertEquals(4, t2.monthDay); - assertEquals(5, t2.month); - assertEquals(2021, t2.year); + assertEquals(Time.TIMEZONE_UTC, t2.getTimezone()); + assertEquals(1, t2.getSecond()); + assertEquals(2, t2.getMinute()); + assertEquals(3, t2.getHour()); + assertEquals(4, t2.getDay()); + assertEquals(5, t2.getMonth()); + assertEquals(2021, t2.getYear()); } @SmallTest public void testSetToNow() { - Time t = new Time(Time.TIMEZONE_UTC); - t.setToNow(); - long ms = t.toMillis(true); long now = System.currentTimeMillis(); - // ensure millis returned are within 1 second of when they were set - assertTrue(ms < now && ms > (now - 1000)); + Time t = new Time(Time.TIMEZONE_UTC); + t.set(now); + long ms = t.toMillis(); + // ensure time is within 1 second because of rounding errors + assertTrue("now: " + now + "; actual: " + ms, Math.abs(ms - now) < 1000); } @SmallTest @@ -280,6 +373,14 @@ public class TimeTest extends TestCase { assertEquals(1, t.getWeekNumber()); t.set(1, 1, 2020); assertEquals(5, t.getWeekNumber()); + + // ensure ISO 8601 standards are met: weeks start on Monday and the first week has at least + // 4 days in it (the year's first Thursday or Jan 4th) + for (int i = 1; i <= 8; i++) { + t.set(i, 0, 2020); + // Jan 6th is the first Monday in 2020 so that would be week 2 + assertEquals(i < 6 ? 1 : 2, t.getWeekNumber()); + } } private static class DateTest { @@ -288,7 +389,6 @@ public class TimeTest extends TestCase { public int day1; public int hour1; public int minute1; - public int dst1; public int offset; @@ -297,7 +397,6 @@ public class TimeTest extends TestCase { public int day2; public int hour2; public int minute2; - public int dst2; public DateTest(int year1, int month1, int day1, int hour1, int minute1, int offset, int year2, int month2, int day2, int hour2, int minute2) { @@ -306,49 +405,30 @@ public class TimeTest extends TestCase { this.day1 = day1; this.hour1 = hour1; this.minute1 = minute1; - this.dst1 = -1; this.offset = offset; this.year2 = year2; this.month2 = month2; this.day2 = day2; this.hour2 = hour2; this.minute2 = minute2; - this.dst2 = -1; - } - - public DateTest(int year1, int month1, int day1, int hour1, int minute1, int dst1, - int offset, int year2, int month2, int day2, int hour2, int minute2, - int dst2) { - this.year1 = year1; - this.month1 = month1; - this.day1 = day1; - this.hour1 = hour1; - this.minute1 = minute1; - this.dst1 = dst1; - this.offset = offset; - this.year2 = year2; - this.month2 = month2; - this.day2 = day2; - this.hour2 = hour2; - this.minute2 = minute2; - this.dst2 = dst2; } public boolean equals(Time time) { - return time.year == year2 && time.month == month2 && time.monthDay == day2 - && time.hour == hour2 && time.minute == minute2; + return time.getYear() == year2 && time.getMonth() == month2 && time.getDay() == day2 + && time.getHour() == hour2 && time.getMinute() == minute2; } + } - public boolean equalsWithDst(Time time) { - return time.year == year2 && time.month == month2 && time.monthDay == day2 - && time.hour == hour2 && time.minute == minute2 && time.isDst == dst2; - } + @SmallTest + public void testNormalize() { + Time t = new Time(Time.TIMEZONE_UTC); + t.parse("20060432T010203"); + assertEquals(1146531723000L, t.normalize()); } /* These tests assume that DST changes on Nov 4, 2007 at 2am (to 1am). */ // The "offset" field in "dayTests" represents days. - // Use normalize(true) with these tests to change the date by 1 day. // Note: the month numbers are 0-relative, so Jan=0, Feb=1,...Dec=11 private DateTest[] dayTests = { // Nov 4, 12am + 0 day = Nov 4, 12am @@ -376,11 +456,12 @@ public class TimeTest extends TestCase { }; // The "offset" field in "minuteTests" represents minutes. - // Use normalize(false) with these tests. // Note: the month numbers are 0-relative, so Jan=0, Feb=1,...Dec=11 private DateTest[] minuteTests = { // Nov 4, 12am + 0 minutes = Nov 4, 12am new DateTest(2007, 10, 4, 0, 0, 0, 2007, 10, 4, 0, 0), + // Nov 4, 12am + 60 minutes = Nov 4, 1am + new DateTest(2007, 10, 4, 0, 0, 60, 2007, 10, 4, 1, 0), // Nov 5, 12am + 0 minutes = Nov 5, 12am new DateTest(2007, 10, 5, 0, 0, 0, 2007, 10, 5, 0, 0), // Nov 3, 12am + 60 minutes = Nov 3, 1am @@ -391,20 +472,24 @@ public class TimeTest extends TestCase { new DateTest(2007, 10, 5, 0, 0, 60, 2007, 10, 5, 1, 0), // Nov 3, 1am + 60 minutes = Nov 3, 2am new DateTest(2007, 10, 3, 1, 0, 60, 2007, 10, 3, 2, 0), + // Nov 4, 12:59am (PDT) + 2 minutes = Nov 4, 1:01am (PDT) + new DateTest(2007, 10, 4, 0, 59, 2, 2007, 10, 4, 1, 1), + // Nov 4, 12:59am (PDT) + 62 minutes = Nov 4, 1:01am (PST) + new DateTest(2007, 10, 4, 0, 59, 62, 2007, 10, 4, 1, 1), + // Nov 4, 12:30am (PDT) + 120 minutes = Nov 4, 1:30am (PST) + new DateTest(2007, 10, 4, 0, 30, 120, 2007, 10, 4, 1, 30), + // Nov 4, 12:30am (PDT) + 90 minutes = Nov 4, 1:00am (PST) + new DateTest(2007, 10, 4, 0, 30, 90, 2007, 10, 4, 1, 0), // Nov 4, 1am (PDT) + 30 minutes = Nov 4, 1:30am (PDT) - new DateTest(2007, 10, 4, 1, 0, 1, 30, 2007, 10, 4, 1, 30, 1), - // Nov 4, 1am (PDT) + 60 minutes = Nov 4, 1am (PST) - new DateTest(2007, 10, 4, 1, 0, 1, 60, 2007, 10, 4, 1, 0, 0), + new DateTest(2007, 10, 4, 1, 0, 30, 2007, 10, 4, 1, 30), // Nov 4, 1:30am (PDT) + 15 minutes = Nov 4, 1:45am (PDT) - new DateTest(2007, 10, 4, 1, 30, 1, 15, 2007, 10, 4, 1, 45, 1), - // Nov 4, 1:30am (PDT) + 30 minutes = Nov 4, 1:00am (PST) - new DateTest(2007, 10, 4, 1, 30, 1, 30, 2007, 10, 4, 1, 0, 0), - // Nov 4, 1:30am (PDT) + 60 minutes = Nov 4, 1:30am (PST) - new DateTest(2007, 10, 4, 1, 30, 1, 60, 2007, 10, 4, 1, 30, 0), + new DateTest(2007, 10, 4, 1, 30, 15, 2007, 10, 4, 1, 45), + // Mar 11, 1:30am (PST) + 30 minutes = Mar 11, 3am (PDT) + new DateTest(2007, 2, 11, 1, 30, 30, 2007, 2, 11, 3, 0), // Nov 4, 1:30am (PST) + 15 minutes = Nov 4, 1:45am (PST) - new DateTest(2007, 10, 4, 1, 30, 0, 15, 2007, 10, 4, 1, 45, 0), + new DateTest(2007, 10, 4, 1, 30, 15, 2007, 10, 4, 1, 45), // Nov 4, 1:30am (PST) + 30 minutes = Nov 4, 2:00am (PST) - new DateTest(2007, 10, 4, 1, 30, 0, 30, 2007, 10, 4, 2, 0, 0), + new DateTest(2007, 10, 4, 1, 30, 30, 2007, 10, 4, 2, 0), // Nov 5, 1am + 60 minutes = Nov 5, 2am new DateTest(2007, 10, 5, 1, 0, 60, 2007, 10, 5, 2, 0), // Nov 3, 2am + 60 minutes = Nov 3, 3am @@ -415,46 +500,35 @@ public class TimeTest extends TestCase { new DateTest(2007, 10, 4, 2, 0, 60, 2007, 10, 4, 3, 0), // Nov 5, 2am + 60 minutes = Nov 5, 3am new DateTest(2007, 10, 5, 2, 0, 60, 2007, 10, 5, 3, 0), + // NOTE: Calendar assumes 1am PDT == 1am PST, the two are not distinct, hence why the transition boundary itself has no tests }; - @SmallTest - public void testNormalize0() { - Time t = new Time(Time.TIMEZONE_UTC); - t.parse("20060432T010203"); - assertEquals(1146531723000L, t.normalize(false)); - } - @MediumTest - public void testNormalize1() { + public void testNormalize_dst() { Time local = new Time("America/Los_Angeles"); int len = dayTests.length; for (int index = 0; index < len; index++) { DateTest test = dayTests[index]; local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1); - // call normalize() to make sure that isDst is set - local.normalize(false); - local.monthDay += test.offset; - local.normalize(true); + local.add(Time.MONTH_DAY, test.offset); if (!test.equals(local)) { String expectedTime = String.format("%d-%02d-%02d %02d:%02d", test.year2, test.month2, test.day2, test.hour2, test.minute2); String actualTime = String.format("%d-%02d-%02d %02d:%02d", - local.year, local.month, local.monthDay, local.hour, local.minute); + local.getYear(), local.getMonth(), local.getDay(), local.getHour(), + local.getMinute()); fail("Expected: " + expectedTime + "; Actual: " + actualTime); } local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1); - // call normalize() to make sure that isDst is set - local.normalize(false); - local.monthDay += test.offset; - long millis = local.toMillis(true); - local.set(millis); + local.add(Time.MONTH_DAY, test.offset); if (!test.equals(local)) { String expectedTime = String.format("%d-%02d-%02d %02d:%02d", test.year2, test.month2, test.day2, test.hour2, test.minute2); String actualTime = String.format("%d-%02d-%02d %02d:%02d", - local.year, local.month, local.monthDay, local.hour, local.minute); + local.getYear(), local.getMonth(), local.getDay(), local.getHour(), + local.getMinute()); fail("Expected: " + expectedTime + "; Actual: " + actualTime); } } @@ -463,40 +537,109 @@ public class TimeTest extends TestCase { for (int index = 0; index < len; index++) { DateTest test = minuteTests[index]; local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1); - local.isDst = test.dst1; - // call normalize() to make sure that isDst is set - local.normalize(false); - if (test.dst2 == -1) test.dst2 = local.isDst; - local.minute += test.offset; - local.normalize(false); - if (!test.equalsWithDst(local)) { - String expectedTime = String.format("%d-%02d-%02d %02d:%02d isDst: %d", - test.year2, test.month2, test.day2, test.hour2, test.minute2, test.dst2); - String actualTime = String.format("%d-%02d-%02d %02d:%02d isDst: %d", - local.year, local.month, local.monthDay, local.hour, local.minute, - local.isDst); + local.add(Time.MINUTE, test.offset); + if (!test.equals(local)) { + String expectedTime = String.format("%d-%02d-%02d %02d:%02d", + test.year2, test.month2, test.day2, test.hour2, test.minute2); + String actualTime = String.format("%d-%02d-%02d %02d:%02d", + local.getYear(), local.getMonth(), local.getDay(), local.getHour(), + local.getMinute()); fail("Expected: " + expectedTime + "; Actual: " + actualTime); } local.set(0, test.minute1, test.hour1, test.day1, test.month1, test.year1); - local.isDst = test.dst1; - // call normalize() to make sure that isDst is set - local.normalize(false); - if (test.dst2 == -1) test.dst2 = local.isDst; - local.minute += test.offset; - long millis = local.toMillis(false); - local.set(millis); - if (!test.equalsWithDst(local)) { - String expectedTime = String.format("%d-%02d-%02d %02d:%02d isDst: %d", - test.year2, test.month2, test.day2, test.hour2, test.minute2, test.dst2); - String actualTime = String.format("%d-%02d-%02d %02d:%02d isDst: %d", - local.year, local.month, local.monthDay, local.hour, local.minute, - local.isDst); + local.add(Time.MINUTE, test.offset); + if (!test.equals(local)) { + String expectedTime = String.format("%d-%02d-%02d %02d:%02d", + test.year2, test.month2, test.day2, test.hour2, test.minute2); + String actualTime = String.format("%d-%02d-%02d %02d:%02d", + local.getYear(), local.getMonth(), local.getDay(), local.getHour(), + local.getMinute()); fail("Expected: " + expectedTime + "; Actual: " + actualTime); } } } + @SmallTest + public void testNormalize_overflow() { + Time t = new Time(Time.TIMEZONE_UTC); + t.set(32, 0, 2020); + t.normalize(); + assertEquals(1, t.getDay()); + assertEquals(1, t.getMonth()); + } + + @SmallTest + public void testDstBehavior_addDays_ignoreDst() { + Time time = new Time("America/Los_Angeles"); + time.set(4, 10, 2007); // set to Nov 4, 2007, 12am + assertEquals(1194159600000L, time.normalize()); + time.add(Time.MONTH_DAY, 1); // changes to Nov 5, 2007, 12am + assertEquals(1194249600000L, time.toMillis()); + + time = new Time("America/Los_Angeles"); + time.set(11, 2, 2007); // set to Mar 11, 2007, 12am + assertEquals(1173600000000L, time.normalize()); + time.add(Time.MONTH_DAY, 1); // changes to Mar 12, 2007, 12am + assertEquals(1173682800000L, time.toMillis()); + } + + @SmallTest + public void testDstBehavior_addDays_applyDst() { + if (!Time.APPLY_DST_CHANGE_LOGIC) { + return; + } + Time time = new Time("America/Los_Angeles"); + time.set(4, 10, 2007); // set to Nov 4, 2007, 12am + assertEquals(1194159600000L, time.normalizeApplyDst()); + time.add(Time.MONTH_DAY, 1); // changes to Nov 4, 2007, 11pm (fall back) + assertEquals(1194246000000L, time.toMillisApplyDst()); + + time = new Time("America/Los_Angeles"); + time.set(11, 2, 2007); // set to Mar 11, 2007, 12am + assertEquals(1173600000000L, time.normalizeApplyDst()); + time.add(Time.MONTH_DAY, 1); // changes to Mar 12, 2007, 1am (roll forward) + assertEquals(1173686400000L, time.toMillisApplyDst()); + } + + @SmallTest + public void testDstBehavior_addHours_ignoreDst() { + // Note: by default, Calendar applies DST logic if adding hours or minutes but not if adding + // days, hence in this test, only if the APPLY_DST_CHANGE_LOGIC flag is false, then the time + // is adjusted with DST + Time time = new Time("America/Los_Angeles"); + time.set(4, 10, 2007); // set to Nov 4, 2007, 12am + assertEquals(1194159600000L, time.normalize()); + time.add(Time.HOUR, 24); // changes to Nov 5, 2007, 12am + assertEquals(Time.APPLY_DST_CHANGE_LOGIC ? 1194249600000L : 1194246000000L, + time.toMillis()); + + time = new Time("America/Los_Angeles"); + time.set(11, 2, 2007); // set to Mar 11, 2007, 12am + assertEquals(1173600000000L, time.normalize()); + time.add(Time.HOUR, 24); // changes to Mar 12, 2007, 12am + assertEquals(Time.APPLY_DST_CHANGE_LOGIC ? 1173682800000L : 1173686400000L, + time.toMillis()); + } + + @SmallTest + public void testDstBehavior_addHours_applyDst() { + if (!Time.APPLY_DST_CHANGE_LOGIC) { + return; + } + Time time = new Time("America/Los_Angeles"); + time.set(4, 10, 2007); // set to Nov 4, 2007, 12am + assertEquals(1194159600000L, time.normalizeApplyDst()); + time.add(Time.HOUR, 24); // changes to Nov 4, 2007, 11pm (fall back) + assertEquals(1194246000000L, time.toMillisApplyDst()); + + time = new Time("America/Los_Angeles"); + time.set(11, 2, 2007); // set to Mar 11, 2007, 12am + assertEquals(1173600000000L, time.normalizeApplyDst()); + time.add(Time.HOUR, 24); // changes to Mar 12, 2007, 1am (roll forward) + assertEquals(1173686400000L, time.toMillisApplyDst()); + } + // Timezones that cover the world. // Some GMT offsets occur more than once in case some cities decide to change their GMT offset. private static final String[] mTimeZones = { @@ -562,32 +705,33 @@ public class TimeTest extends TestCase { @MediumTest public void testGetJulianDay() { - Time time = new Time(); - - // for a random day in the year 2020 and for a random timezone, get the Julian day for 12am - // and then check that if we change the time we get the same Julian day. - int monthDay = (int) (Math.random() * 365) + 1; - int zoneIndex = (int) (Math.random() * mTimeZones.length); - time.set(0, 0, 0, monthDay, 0, 2020); - time.timezone = mTimeZones[zoneIndex]; - long millis = time.normalize(true); - - int julianDay = Time.getJulianDay(millis, time.gmtoff); - - // change the time during the day and check that we get the same Julian day. - for (int hour = 0; hour < 24; hour++) { - for (int minute = 0; minute < 60; minute += 15) { - time.set(0, minute, hour, monthDay, 0, 2020); - millis = time.normalize(true); - int day = Time.getJulianDay(millis, time.gmtoff); - assertEquals(day, julianDay); + Time time = new Time(Time.TIMEZONE_UTC); + + // for 30 random days in the year 2020 and for a random timezone, get the Julian day for + // 12am and then check that if we change the time we get the same Julian day. + for (int i = 0; i < 30; i++) { + int monthDay = (int) (Math.random() * 365) + 1; + int zoneIndex = (int) (Math.random() * mTimeZones.length); + time.setTimezone(mTimeZones[zoneIndex]); + time.set(0, 0, 0, monthDay, 0, 2020); + + int julianDay = Time.getJulianDay(time.normalize(), time.getGmtOffset()); + + // change the time during the day and check that we get the same Julian day. + for (int hour = 0; hour < 24; hour++) { + for (int minute = 0; minute < 60; minute += 15) { + time.set(0, minute, hour, monthDay, 0, 2020); + int day = Time.getJulianDay(time.normalize(), time.getGmtOffset()); + assertEquals(day, julianDay); + time.clear(Time.TIMEZONE_UTC); + } } } } @MediumTest public void testSetJulianDay() { - Time time = new Time(); + Time time = new Time(Time.TIMEZONE_UTC); // for each day in the year 2020, pick a random timezone, and verify that we can // set the Julian day correctly. @@ -596,22 +740,23 @@ public class TimeTest extends TestCase { // leave the "month" as zero because we are changing the "monthDay" from 1 to 366. // the call to normalize() will then change the "month" (but we don't really care). time.set(0, 0, 0, monthDay, 0, 2020); - time.timezone = mTimeZones[zoneIndex]; - long millis = time.normalize(true); - int julianDay = Time.getJulianDay(millis, time.gmtoff); + time.setTimezone(mTimeZones[zoneIndex]); + long millis = time.normalize(); + int julianDay = Time.getJulianDay(millis, time.getGmtOffset()); time.setJulianDay(julianDay); // some places change daylight saving time at 12am and so there is no 12am on some days // in some timezones - in those cases, the time is set to 1am. // some examples: Africa/Cairo, America/Sao_Paulo, Atlantic/Azores - assertTrue(time.hour == 0 || time.hour == 1); - assertEquals(0, time.minute); - assertEquals(0, time.second); + assertTrue(time.getHour() == 0 || time.getHour() == 1); + assertEquals(0, time.getMinute()); + assertEquals(0, time.getSecond()); - millis = time.toMillis(false); - int day = Time.getJulianDay(millis, time.gmtoff); + millis = time.toMillis(); + int day = Time.getJulianDay(millis, time.getGmtOffset()); assertEquals(day, julianDay); + time.clear(Time.TIMEZONE_UTC); } } } |