aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2021-10-07 23:50:28 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2021-10-07 23:50:28 +0000
commit2d9cb9aa925d7e7d11bb44719bf3e4536d9a8075 (patch)
treec9dc8dfb27aeebd6daf5c8c04627e9bd0d18b961
parent907ed0923b996d5ad5b20f39b7d37529761fb48d (diff)
parentd509f4e1d4d3ce26036e4f914f9fdd56964c3356 (diff)
downloadcalendar-2d9cb9aa925d7e7d11bb44719bf3e4536d9a8075.tar.gz
-rw-r--r--src/com/android/calendarcommon2/EventRecurrence.java6
-rw-r--r--src/com/android/calendarcommon2/RecurrenceProcessor.java156
-rw-r--r--src/com/android/calendarcommon2/RecurrenceSet.java44
-rw-r--r--src/com/android/calendarcommon2/Time.java532
-rw-r--r--tests/src/com/android/calendarcommon2/RRuleTest.java4
-rw-r--r--tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java35
-rw-r--r--tests/src/com/android/calendarcommon2/TimeTest.java762
7 files changed, 1404 insertions, 135 deletions
diff --git a/src/com/android/calendarcommon2/EventRecurrence.java b/src/com/android/calendarcommon2/EventRecurrence.java
index 45fd12f..d54f2fe 100644
--- a/src/com/android/calendarcommon2/EventRecurrence.java
+++ b/src/com/android/calendarcommon2/EventRecurrence.java
@@ -17,9 +17,7 @@
package com.android.calendarcommon2;
import android.text.TextUtils;
-import android.text.format.Time;
import android.util.Log;
-import android.util.TimeFormatException;
import java.util.Calendar;
import java.util.HashMap;
@@ -476,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 &&
@@ -740,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 d0a647a..24decce 100644
--- a/src/com/android/calendarcommon2/RecurrenceProcessor.java
+++ b/src/com/android/calendarcommon2/RecurrenceProcessor.java
@@ -17,7 +17,6 @@
package com.android.calendarcommon2;
-import android.text.format.Time;
import android.util.Log;
import java.util.TreeSet;
@@ -93,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;
}
@@ -129,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.
@@ -201,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;
}
@@ -223,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;
}
@@ -231,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;
@@ -243,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;
@@ -255,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;
@@ -264,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;
@@ -273,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;
@@ -326,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
@@ -368,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 {
@@ -429,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());
}
}
@@ -466,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);
}
}
@@ -507,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 {
@@ -631,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);
@@ -703,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;
}
@@ -728,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,
@@ -827,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);
}
}
@@ -847,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;
@@ -876,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
@@ -923,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
@@ -1065,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);
@@ -1103,7 +1087,7 @@ bysetpos:
if (freqField != Time.YEAR && freqField != Time.MONTH) {
break;
}
- if (iterator.monthDay == oldDay) {
+ if (iterator.getDay() == oldDay) {
break;
}
n++;
@@ -1136,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;
@@ -1202,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));
}
/**
@@ -1300,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 86e6a2d..e42c0e9 100644
--- a/src/com/android/calendarcommon2/RecurrenceSet.java
+++ b/src/com/android/calendarcommon2/RecurrenceSet.java
@@ -20,9 +20,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.provider.CalendarContract;
import android.text.TextUtils;
-import android.text.format.Time;
import android.util.Log;
-import android.util.TimeFormatException;
import java.util.ArrayList;
import java.util.List;
@@ -162,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;
}
@@ -196,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
@@ -224,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) {
@@ -243,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;
@@ -301,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());
@@ -360,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());
@@ -480,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
new file mode 100644
index 0000000..f0af248
--- /dev/null
+++ b/src/com/android/calendarcommon2/Time.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package 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.
+ */
+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 = 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() {
+ this(TimeZone.getDefault().getID());
+ }
+
+ public Time(String timezone) {
+ 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 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 writeFieldsToCalendar() {
+ clearCalendar();
+ mCalendar.set(year, month, monthDay, hour, minute, second);
+ mCalendar.set(Calendar.MILLISECOND, 0);
+ }
+
+ private boolean isInDst() {
+ return mCalendar.getTimeZone().inDaylightTime(mCalendar.getTime());
+ }
+
+ 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) {
+ clearCalendar();
+ mCalendar.setTimeInMillis(millis);
+ readFieldsFromCalendar();
+ }
+
+ public void set(Time other) {
+ clearCalendar();
+ mCalendar.setTimeZone(other.getTimeZone());
+ mCalendar.setTimeInMillis(other.mCalendar.getTimeInMillis());
+ readFieldsFromCalendar();
+ }
+
+ public void set(int day, int month, int year) {
+ clearCalendar();
+ mCalendar.set(year, month, day);
+ readFieldsFromCalendar();
+ }
+
+ public void set(int second, int minute, int hour, int day, int month, int year) {
+ clearCalendar();
+ mCalendar.set(year, month, day, hour, minute, second);
+ readFieldsFromCalendar();
+ }
+
+ public long setJulianDay(int julianDay) {
+ 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) {
+ return android.text.format.Time.getJulianDay(begin, gmtOff);
+ }
+
+ public int getWeekNumber() {
+ 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) {
+ return mCalendar.getActualMaximum(getCalendarField(field));
+ }
+
+ public void switchTimezone(String timezone) {
+ long msBefore = mCalendar.getTimeInMillis();
+ mCalendar.setTimeZone(TimeZone.getTimeZone(timezone));
+ mCalendar.setTimeInMillis(msBefore);
+ mDstChangedByField = -1;
+ readFieldsFromCalendar();
+ }
+
+ /**
+ * @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;
+ }
+
+ private long normalizeInternal() {
+ final long ms = mCalendar.getTimeInMillis();
+ readFieldsFromCalendar();
+ return ms;
+ }
+
+ public long normalize() {
+ return getDstAdjustedMillis(false, normalizeInternal());
+ }
+
+ 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() {
+ 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 long toMillis() {
+ return getDstAdjustedMillis(false, mCalendar.getTimeInMillis());
+ }
+
+ long toMillisApplyDst() {
+ return getDstAdjustedMillis(true, mCalendar.getTimeInMillis());
+ }
+
+ private TimeZone getTimeZone() {
+ return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault();
+ }
+
+ 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) {
+ 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 1d72366..18217a3 100644
--- a/tests/src/com/android/calendarcommon2/RRuleTest.java
+++ b/tests/src/com/android/calendarcommon2/RRuleTest.java
@@ -24,7 +24,6 @@ import com.android.calendarcommon2.RecurrenceSet;
import android.os.Debug;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.Suppress;
-import android.text.format.Time;
import junit.framework.TestCase;
/**
@@ -115,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 3503aae..3cd9177 100644
--- a/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java
+++ b/tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java
@@ -22,9 +22,7 @@ import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.TextUtils;
-import android.text.format.Time;
import android.util.Log;
-import android.util.TimeFormatException;
import junit.framework.TestCase;
import java.util.TreeSet;
@@ -106,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();
@@ -150,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) {
@@ -598,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
}
}
@@ -2460,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++) {
@@ -2504,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();
@@ -2521,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
new file mode 100644
index 0000000..df27c4f
--- /dev/null
+++ b/tests/src/com/android/calendarcommon2/TimeTest.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.calendarcommon2;
+
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.TimeFormatException;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for com.android.calendarcommon2.Time.
+ *
+ * Some of these tests are borrowed from android.text.format.TimeTest.
+ */
+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.getTimezone());
+ }
+
+ @SmallTest
+ public void testSwitchTimezone() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ String newTimezone = "America/Los_Angeles";
+ t.switchTimezone(newTimezone);
+ assertEquals(newTimezone, t.getTimezone());
+ }
+
+ @SmallTest
+ public void testGetActualMaximum() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ 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(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
+ public void testClear() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ t.clear(Time.TIMEZONE_UTC);
+
+ 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(a.compareTo(b) < 0);
+
+ 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();
+ assertEquals("19700101T000000", t.format2445());
+ t.setTimezone(Time.TIMEZONE_UTC);
+ assertEquals("19700101T000000Z", t.format2445());
+ t.setAllDay(true);
+ assertEquals("19700101", t.format2445());
+ }
+
+ @SmallTest
+ public void testFormat3339() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ assertEquals("1970-01-01", t.format3339(true));
+ t.set(29, 1, 2020);
+ assertEquals("2020-02-29", t.format3339(true));
+ }
+
+ @SmallTest
+ 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());
+ t.set(1, 0, 0, 1, 1, 2020);
+ 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 testToMillis_overflow() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ 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);
+ 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
+ public void testParse3339() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+
+ t.parse3339("1980-05-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.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.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.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.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.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.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.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");
+ }
+
+ try {
+ t.parse3339("1980");
+ fail("Did not throw error on truncated input length");
+ } catch (TimeFormatException e) {
+ // successful
+ }
+
+ try {
+ t.parse3339("1980-05-23T09:50:50.123+");
+ fail("Did not throw error on truncated timezone offset");
+ } catch (TimeFormatException e1) {
+ // successful
+ }
+
+ try {
+ t.parse3339("1980-05-23T09:50:50.123+05:0");
+ fail("Did not throw error on truncated timezone offset");
+ } catch (TimeFormatException e1) {
+ // successful
+ }
+ }
+
+ @SmallTest
+ public void testSet_millis() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+
+ t.set(1000L);
+ assertEquals(1970, t.getYear());
+ assertEquals(1, t.getSecond());
+
+ t.set(2000L);
+ assertEquals(2, t.getSecond());
+ assertEquals(0, t.getMinute());
+
+ t.set(1000L * 60);
+ assertEquals(1, t.getMinute());
+ assertEquals(0, t.getHour());
+
+ t.set(1000L * 60 * 60);
+ assertEquals(1, t.getHour());
+ assertEquals(1, t.getDay());
+
+ t.set((1000L * 60 * 60 * 24) + 1000L);
+ assertEquals(2, t.getDay());
+ assertEquals(1970, t.getYear());
+ }
+
+ @SmallTest
+ public void testSet_dayMonthYear() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ t.set(1, 2, 2021);
+ assertEquals(1, t.getDay());
+ assertEquals(2, t.getMonth());
+ assertEquals(2021, t.getYear());
+ }
+
+ @SmallTest
+ public void testSet_secondMinuteHour() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ t.set(1, 2, 3, 4, 5, 2021);
+ 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 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.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() {
+ long now = System.currentTimeMillis();
+ 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
+ public void testGetWeekNumber() {
+ Time t = new Time(Time.TIMEZONE_UTC);
+ t.set(1000L);
+ 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 {
+ public int year1;
+ public int month1;
+ public int day1;
+ public int hour1;
+ public int minute1;
+
+ public int offset;
+
+ public int year2;
+ public int month2;
+ public int day2;
+ public int hour2;
+ public int minute2;
+
+ public DateTest(int year1, int month1, int day1, int hour1, int minute1,
+ int offset, int year2, int month2, int day2, int hour2, int minute2) {
+ this.year1 = year1;
+ this.month1 = month1;
+ this.day1 = day1;
+ this.hour1 = hour1;
+ this.minute1 = minute1;
+ this.offset = offset;
+ this.year2 = year2;
+ this.month2 = month2;
+ this.day2 = day2;
+ this.hour2 = hour2;
+ this.minute2 = minute2;
+ }
+
+ public boolean equals(Time time) {
+ return time.getYear() == year2 && time.getMonth() == month2 && time.getDay() == day2
+ && time.getHour() == hour2 && time.getMinute() == minute2;
+ }
+ }
+
+ @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.
+ // 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
+ new DateTest(2007, 10, 4, 0, 0, 0, 2007, 10, 4, 0, 0),
+ // Nov 5, 12am + 0 day = Nov 5, 12am
+ new DateTest(2007, 10, 5, 0, 0, 0, 2007, 10, 5, 0, 0),
+ // Nov 3, 12am + 1 day = Nov 4, 12am
+ new DateTest(2007, 10, 3, 0, 0, 1, 2007, 10, 4, 0, 0),
+ // Nov 4, 12am + 1 day = Nov 5, 12am
+ new DateTest(2007, 10, 4, 0, 0, 1, 2007, 10, 5, 0, 0),
+ // Nov 5, 12am + 1 day = Nov 6, 12am
+ new DateTest(2007, 10, 5, 0, 0, 1, 2007, 10, 6, 0, 0),
+ // Nov 3, 1am + 1 day = Nov 4, 1am
+ new DateTest(2007, 10, 3, 1, 0, 1, 2007, 10, 4, 1, 0),
+ // Nov 4, 1am + 1 day = Nov 5, 1am
+ new DateTest(2007, 10, 4, 1, 0, 1, 2007, 10, 5, 1, 0),
+ // Nov 5, 1am + 1 day = Nov 6, 1am
+ new DateTest(2007, 10, 5, 1, 0, 1, 2007, 10, 6, 1, 0),
+ // Nov 3, 2am + 1 day = Nov 4, 2am
+ new DateTest(2007, 10, 3, 2, 0, 1, 2007, 10, 4, 2, 0),
+ // Nov 4, 2am + 1 day = Nov 5, 2am
+ new DateTest(2007, 10, 4, 2, 0, 1, 2007, 10, 5, 2, 0),
+ // Nov 5, 2am + 1 day = Nov 6, 2am
+ new DateTest(2007, 10, 5, 2, 0, 1, 2007, 10, 6, 2, 0),
+ };
+
+ // The "offset" field in "minuteTests" represents minutes.
+ // 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
+ new DateTest(2007, 10, 3, 0, 0, 60, 2007, 10, 3, 1, 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 + 60 minutes = Nov 5, 1am
+ 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, 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, 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, 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, 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
+ new DateTest(2007, 10, 3, 2, 0, 60, 2007, 10, 3, 3, 0),
+ // Nov 4, 2am + 30 minutes = Nov 4, 2:30am
+ new DateTest(2007, 10, 4, 2, 0, 30, 2007, 10, 4, 2, 30),
+ // Nov 4, 2am + 60 minutes = Nov 4, 3am
+ 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
+ };
+
+ @MediumTest
+ 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);
+ 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.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.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.getYear(), local.getMonth(), local.getDay(), local.getHour(),
+ local.getMinute());
+ fail("Expected: " + expectedTime + "; Actual: " + actualTime);
+ }
+ }
+
+ len = minuteTests.length;
+ 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.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.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 = {
+ "Pacific/Kiritimati",
+ "Pacific/Enderbury",
+ "Pacific/Fiji",
+ "Antarctica/South_Pole",
+ "Pacific/Norfolk",
+ "Pacific/Ponape",
+ "Asia/Magadan",
+ "Australia/Lord_Howe",
+ "Australia/Sydney",
+ "Australia/Adelaide",
+ "Asia/Tokyo",
+ "Asia/Seoul",
+ "Asia/Taipei",
+ "Asia/Singapore",
+ "Asia/Hong_Kong",
+ "Asia/Saigon",
+ "Asia/Bangkok",
+ "Indian/Cocos",
+ "Asia/Rangoon",
+ "Asia/Omsk",
+ "Antarctica/Mawson",
+ "Asia/Colombo",
+ "Asia/Calcutta",
+ "Asia/Oral",
+ "Asia/Kabul",
+ "Asia/Dubai",
+ "Asia/Tehran",
+ "Europe/Moscow",
+ "Asia/Baghdad",
+ "Africa/Mogadishu",
+ "Europe/Athens",
+ "Africa/Cairo",
+ "Europe/Rome",
+ "Europe/Berlin",
+ "Europe/Amsterdam",
+ "Africa/Tunis",
+ "Europe/London",
+ "Europe/Dublin",
+ "Atlantic/St_Helena",
+ "Africa/Monrovia",
+ "Africa/Accra",
+ "Atlantic/Azores",
+ "Atlantic/South_Georgia",
+ "America/Noronha",
+ "America/Sao_Paulo",
+ "America/Cayenne",
+ "America/St_Johns",
+ "America/Puerto_Rico",
+ "America/Aruba",
+ "America/New_York",
+ "America/Chicago",
+ "America/Denver",
+ "America/Los_Angeles",
+ "America/Anchorage",
+ "Pacific/Marquesas",
+ "America/Adak",
+ "Pacific/Honolulu",
+ "Pacific/Midway",
+ };
+
+ @MediumTest
+ public void testGetJulianDay() {
+ 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.TIMEZONE_UTC);
+
+ // for each day in the year 2020, pick a random timezone, and verify that we can
+ // set the Julian day correctly.
+ for (int monthDay = 1; monthDay <= 366; monthDay++) {
+ int zoneIndex = (int) (Math.random() * mTimeZones.length);
+ // 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.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.getHour() == 0 || time.getHour() == 1);
+ assertEquals(0, time.getMinute());
+ assertEquals(0, time.getSecond());
+
+ millis = time.toMillis();
+ int day = Time.getJulianDay(millis, time.getGmtOffset());
+ assertEquals(day, julianDay);
+ time.clear(Time.TIMEZONE_UTC);
+ }
+ }
+}