diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2021-06-19 12:05:11 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2021-06-19 12:05:11 +0000 |
commit | d261ccc3a851b370a50902bc80246c0877307fdb (patch) | |
tree | c9dc8dfb27aeebd6daf5c8c04627e9bd0d18b961 | |
parent | 6e29ad1dfe9b353af1c41a30101673bf01d7a144 (diff) | |
parent | f465350c4d285fa131629b1e229dfc1bf113d1be (diff) | |
download | calendar-d261ccc3a851b370a50902bc80246c0877307fdb.tar.gz |
Snap for 7474514 from f465350c4d285fa131629b1e229dfc1bf113d1be to mainline-media-releaseandroid-mainline-12.0.0_r89android-mainline-12.0.0_r74android-mainline-12.0.0_r62android-mainline-12.0.0_r46android-mainline-12.0.0_r29android-mainline-12.0.0_r12android-mainline-12.0.0_r119android-mainline-12.0.0_r104android12-mainline-media-release
Change-Id: I152dc0fcd070e673d7a9e3b1ed8c3f82b45e672c
-rw-r--r-- | Android.bp | 4 | ||||
-rw-r--r-- | src/com/android/calendarcommon2/EventRecurrence.java | 6 | ||||
-rw-r--r-- | src/com/android/calendarcommon2/RecurrenceProcessor.java | 156 | ||||
-rw-r--r-- | src/com/android/calendarcommon2/RecurrenceSet.java | 44 | ||||
-rw-r--r-- | src/com/android/calendarcommon2/Time.java | 532 | ||||
-rw-r--r-- | tests/Android.bp | 4 | ||||
-rw-r--r-- | tests/src/com/android/calendarcommon2/RRuleTest.java | 4 | ||||
-rw-r--r-- | tests/src/com/android/calendarcommon2/RecurrenceProcessorTest.java | 35 | ||||
-rw-r--r-- | tests/src/com/android/calendarcommon2/TimeTest.java | 762 |
9 files changed, 1412 insertions, 135 deletions
@@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_library { name: "calendar-common", sdk_version: "15", 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/Android.bp b/tests/Android.bp index cdda049..938b51c 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test { name: "CalendarCommonTests", sdk_version: "current", 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); + } + } +} |