1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.format;
18 
19 import android.util.TimeFormatException;
20 
21 import com.android.i18n.timezone.ZoneInfoData;
22 import com.android.i18n.timezone.ZoneInfoDb;
23 
24 import java.util.Locale;
25 import java.util.TimeZone;
26 
27 /**
28  * An alternative to the {@link java.util.Calendar} and
29  * {@link java.util.GregorianCalendar} classes. An instance of the Time class represents
30  * a moment in time, specified with second precision. It is modelled after
31  * struct tm. This class is not thread-safe and does not consider leap seconds.
32  *
33  * <p>This class has a number of issues and it is recommended that
34  * {@link java.util.GregorianCalendar} is used instead.
35  *
36  * <p>Known issues:
37  * <ul>
38  *     <li>For historical reasons when performing time calculations all arithmetic currently takes
39  *     place using 32-bit integers. This limits the reliable time range representable from 1902
40  *     until 2037.See the wikipedia article on the
41  *     <a href="http://en.wikipedia.org/wiki/Year_2038_problem">Year 2038 problem</a> for details.
42  *     Do not rely on this behavior; it may change in the future.
43  *     </li>
44  *     <li>Calling {@link #switchTimezone(String)} on a date that cannot exist, such as a wall time
45  *     that was skipped due to a DST transition, will result in a date in 1969 (i.e. -1, or 1 second
46  *     before 1st Jan 1970 UTC).</li>
47  *     <li>Much of the formatting / parsing assumes ASCII text and is therefore not suitable for
48  *     use with non-ASCII scripts.</li>
49  *     <li>No support for pseudo-zones like "GMT-07:00".</li>
50  * </ul>
51  *
52  * @deprecated Use {@link java.util.GregorianCalendar} instead.
53  */
54 @Deprecated
55 public class Time {
56     private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000";
57     private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z";
58     private static final String Y_M_D = "%Y-%m-%d";
59 
60     public static final String TIMEZONE_UTC = "UTC";
61 
62     /**
63      * The Julian day of the epoch, that is, January 1, 1970 on the Gregorian
64      * calendar.
65      */
66     public static final int EPOCH_JULIAN_DAY = 2440588;
67 
68     /**
69      * The Julian day of the Monday in the week of the epoch, December 29, 1969
70      * on the Gregorian calendar.
71      */
72     public static final int MONDAY_BEFORE_JULIAN_EPOCH = EPOCH_JULIAN_DAY - 3;
73 
74     /**
75      * True if this is an allDay event. The hour, minute, second fields are
76      * all zero, and the date is displayed the same in all time zones.
77      */
78     public boolean allDay;
79 
80     /**
81      * Seconds [0-61] (2 leap seconds allowed)
82      */
83     public int second;
84 
85     /**
86      * Minute [0-59]
87      */
88     public int minute;
89 
90     /**
91      * Hour of day [0-23]
92      */
93     public int hour;
94 
95     /**
96      * Day of month [1-31]
97      */
98     public int monthDay;
99 
100     /**
101      * Month [0-11]
102      */
103     public int month;
104 
105     /**
106      * Year. For example, 1970.
107      */
108     public int year;
109 
110     /**
111      * Day of week [0-6]
112      */
113     public int weekDay;
114 
115     /**
116      * Day of year [0-365]
117      */
118     public int yearDay;
119 
120     /**
121      * This time is in daylight savings time. One of:
122      * <ul>
123      * <li><b>positive</b> - in dst</li>
124      * <li><b>0</b> - not in dst</li>
125      * <li><b>negative</b> - unknown</li>
126      * </ul>
127      */
128     public int isDst;
129 
130     /**
131      * Offset in seconds from UTC including any DST offset.
132      */
133     public long gmtoff;
134 
135     /**
136      * The timezone for this Time.  Should not be null.
137      */
138     public String timezone;
139 
140     /*
141      * Define symbolic constants for accessing the fields in this class. Used in
142      * getActualMaximum().
143      */
144     public static final int SECOND = 1;
145     public static final int MINUTE = 2;
146     public static final int HOUR = 3;
147     public static final int MONTH_DAY = 4;
148     public static final int MONTH = 5;
149     public static final int YEAR = 6;
150     public static final int WEEK_DAY = 7;
151     public static final int YEAR_DAY = 8;
152     public static final int WEEK_NUM = 9;
153 
154     public static final int SUNDAY = 0;
155     public static final int MONDAY = 1;
156     public static final int TUESDAY = 2;
157     public static final int WEDNESDAY = 3;
158     public static final int THURSDAY = 4;
159     public static final int FRIDAY = 5;
160     public static final int SATURDAY = 6;
161 
162     // An object that is reused for date calculations.
163     private TimeCalculator calculator;
164 
165     /**
166      * Construct a Time object in the timezone named by the string
167      * argument "timezone". The time is initialized to Jan 1, 1970.
168      * @param timezoneId string containing the timezone to use.
169      * @see TimeZone
170      */
Time(String timezoneId)171     public Time(String timezoneId) {
172         if (timezoneId == null) {
173             throw new NullPointerException("timezoneId is null!");
174         }
175         initialize(timezoneId);
176     }
177 
178     /**
179      * Construct a Time object in the default timezone. The time is initialized to
180      * Jan 1, 1970.
181      */
Time()182     public Time() {
183         initialize(TimeZone.getDefault().getID());
184     }
185 
186     /**
187      * A copy constructor.  Construct a Time object by copying the given
188      * Time object.  No normalization occurs.
189      *
190      * @param other
191      */
Time(Time other)192     public Time(Time other) {
193         initialize(other.timezone);
194         set(other);
195     }
196 
197     /** Initialize the Time to 00:00:00 1/1/1970 in the specified timezone. */
initialize(String timezoneId)198     private void initialize(String timezoneId) {
199         this.timezone = timezoneId;
200         this.year = 1970;
201         this.monthDay = 1;
202         // Set the daylight-saving indicator to the unknown value -1 so that
203         // it will be recomputed.
204         this.isDst = -1;
205 
206         // A reusable object that performs the date/time calculations.
207         calculator = new TimeCalculator(timezoneId);
208     }
209 
210     /**
211      * Ensures the values in each field are in range. For example if the
212      * current value of this calendar is March 32, normalize() will convert it
213      * to April 1. It also fills in weekDay, yearDay, isDst and gmtoff.
214      *
215      * <p>
216      * If "ignoreDst" is true, then this method sets the "isDst" field to -1
217      * (the "unknown" value) before normalizing.  It then computes the
218      * time in milliseconds and sets the correct value for "isDst" if the
219      * fields resolve to a valid date / time.
220      *
221      * <p>
222      * See {@link #toMillis(boolean)} for more information about when to
223      * use <tt>true</tt> or <tt>false</tt> for "ignoreDst" and when {@code -1}
224      * might be returned.
225      *
226      * @return the UTC milliseconds since the epoch, or {@code -1}
227      */
normalize(boolean ignoreDst)228     public long normalize(boolean ignoreDst) {
229         calculator.copyFieldsFromTime(this);
230         long timeInMillis = calculator.toMillis(ignoreDst);
231         calculator.copyFieldsToTime(this);
232         return timeInMillis;
233     }
234 
235     /**
236      * Convert this time object so the time represented remains the same, but is
237      * instead located in a different timezone. This method automatically calls
238      * normalize() in some cases.
239      *
240      * <p>This method can return incorrect results if the date / time cannot be normalized.
241      */
switchTimezone(String timezone)242     public void switchTimezone(String timezone) {
243         calculator.copyFieldsFromTime(this);
244         calculator.switchTimeZone(timezone);
245         calculator.copyFieldsToTime(this);
246         this.timezone = timezone;
247     }
248 
249     private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31,
250             31, 30, 31, 30, 31 };
251 
252     /**
253      * Return the maximum possible value for the given field given the value of
254      * the other fields. Requires that it be normalized for MONTH_DAY and
255      * YEAR_DAY.
256      * @param field one of the constants for HOUR, MINUTE, SECOND, etc.
257      * @return the maximum value for the field.
258      */
getActualMaximum(int field)259     public int getActualMaximum(int field) {
260         switch (field) {
261         case SECOND:
262             return 59; // leap seconds, bah humbug
263         case MINUTE:
264             return 59;
265         case HOUR:
266             return 23;
267         case MONTH_DAY: {
268             int n = DAYS_PER_MONTH[this.month];
269             if (n != 28) {
270                 return n;
271             } else {
272                 int y = this.year;
273                 return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 29 : 28;
274             }
275         }
276         case MONTH:
277             return 11;
278         case YEAR:
279             return 2037;
280         case WEEK_DAY:
281             return 6;
282         case YEAR_DAY: {
283             int y = this.year;
284             // Year days are numbered from 0, so the last one is usually 364.
285             return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 365 : 364;
286         }
287         case WEEK_NUM:
288             throw new RuntimeException("WEEK_NUM not implemented");
289         default:
290             throw new RuntimeException("bad field=" + field);
291         }
292     }
293 
294     /**
295      * Clears all values, setting the timezone to the given timezone. Sets isDst
296      * to a negative value to mean "unknown".
297      * @param timezoneId the timezone to use.
298      */
clear(String timezoneId)299     public void clear(String timezoneId) {
300         if (timezoneId == null) {
301             throw new NullPointerException("timezone is null!");
302         }
303         this.timezone = timezoneId;
304         this.allDay = false;
305         this.second = 0;
306         this.minute = 0;
307         this.hour = 0;
308         this.monthDay = 0;
309         this.month = 0;
310         this.year = 0;
311         this.weekDay = 0;
312         this.yearDay = 0;
313         this.gmtoff = 0;
314         this.isDst = -1;
315     }
316 
317     /**
318      * Compare two {@code Time} objects and return a negative number if {@code
319      * a} is less than {@code b}, a positive number if {@code a} is greater than
320      * {@code b}, or 0 if they are equal.
321      *
322      * <p>
323      * This method can return an incorrect answer when the date / time fields of
324      * either {@code Time} have been set to a local time that contradicts the
325      * available timezone information.
326      *
327      * @param a first {@code Time} instance to compare
328      * @param b second {@code Time} instance to compare
329      * @throws NullPointerException if either argument is {@code null}
330      * @throws IllegalArgumentException if {@link #allDay} is true but {@code
331      *             hour}, {@code minute}, and {@code second} are not 0.
332      * @return a negative result if {@code a} is earlier, a positive result if
333      *         {@code b} is earlier, or 0 if they are equal.
334      */
compare(Time a, Time b)335     public static int compare(Time a, Time b) {
336         if (a == null) {
337             throw new NullPointerException("a == null");
338         } else if (b == null) {
339             throw new NullPointerException("b == null");
340         }
341         a.calculator.copyFieldsFromTime(a);
342         b.calculator.copyFieldsFromTime(b);
343 
344         return TimeCalculator.compare(a.calculator, b.calculator);
345     }
346 
347     /**
348      * Print the current value given the format string provided. See man
349      * strftime for what means what. The final string must be less than 256
350      * characters.
351      * @param format a string containing the desired format.
352      * @return a String containing the current time expressed in the current locale.
353      */
format(String format)354     public String format(String format) {
355         calculator.copyFieldsFromTime(this);
356         return calculator.format(format);
357     }
358 
359     /**
360      * Return the current time in YYYYMMDDTHHMMSS&lt;tz&gt; format
361      */
362     @Override
toString()363     public String toString() {
364         // toString() uses its own TimeCalculator rather than the shared one. Otherwise weird stuff
365         // happens during debugging when the debugger calls toString().
366         TimeCalculator calculator = new TimeCalculator(this.timezone);
367         calculator.copyFieldsFromTime(this);
368         return calculator.toStringInternal();
369     }
370 
371     /**
372      * Parses a date-time string in either the RFC 2445 format or an abbreviated
373      * format that does not include the "time" field.  For example, all of the
374      * following strings are valid:
375      *
376      * <ul>
377      *   <li>"20081013T160000Z"</li>
378      *   <li>"20081013T160000"</li>
379      *   <li>"20081013"</li>
380      * </ul>
381      *
382      * Returns whether or not the time is in UTC (ends with Z).  If the string
383      * ends with "Z" then the timezone is set to UTC.  If the date-time string
384      * included only a date and no time field, then the <code>allDay</code>
385      * field of this Time class is set to true and the <code>hour</code>,
386      * <code>minute</code>, and <code>second</code> fields are set to zero;
387      * otherwise (a time field was included in the date-time string)
388      * <code>allDay</code> is set to false. The fields <code>weekDay</code>,
389      * <code>yearDay</code>, and <code>gmtoff</code> are always set to zero,
390      * and the field <code>isDst</code> is set to -1 (unknown).  To set those
391      * fields, call {@link #normalize(boolean)} after parsing.
392      *
393      * To parse a date-time string and convert it to UTC milliseconds, do
394      * something like this:
395      *
396      * <pre>
397      *   Time time = new Time();
398      *   String date = "20081013T160000Z";
399      *   time.parse(date);
400      *   long millis = time.normalize(false);
401      * </pre>
402      *
403      * @param s the string to parse
404      * @return true if the resulting time value is in UTC time
405      * @throws android.util.TimeFormatException if s cannot be parsed.
406      */
parse(String s)407     public boolean parse(String s) {
408         if (s == null) {
409             throw new NullPointerException("time string is null");
410         }
411         if (parseInternal(s)) {
412             timezone = TIMEZONE_UTC;
413             return true;
414         }
415         return false;
416     }
417 
418     /**
419      * Parse a time in the current zone in YYYYMMDDTHHMMSS format.
420      */
parseInternal(String s)421     private boolean parseInternal(String s) {
422         int len = s.length();
423         if (len < 8) {
424             throw new TimeFormatException("String is too short: \"" + s +
425                     "\" Expected at least 8 characters.");
426         }
427 
428         boolean inUtc = false;
429 
430         // year
431         int n = getChar(s, 0, 1000);
432         n += getChar(s, 1, 100);
433         n += getChar(s, 2, 10);
434         n += getChar(s, 3, 1);
435         year = n;
436 
437         // month
438         n = getChar(s, 4, 10);
439         n += getChar(s, 5, 1);
440         n--;
441         month = n;
442 
443         // day of month
444         n = getChar(s, 6, 10);
445         n += getChar(s, 7, 1);
446         monthDay = n;
447 
448         if (len > 8) {
449             if (len < 15) {
450                 throw new TimeFormatException(
451                         "String is too short: \"" + s
452                                 + "\" If there are more than 8 characters there must be at least"
453                                 + " 15.");
454             }
455             checkChar(s, 8, 'T');
456             allDay = false;
457 
458             // hour
459             n = getChar(s, 9, 10);
460             n += getChar(s, 10, 1);
461             hour = n;
462 
463             // min
464             n = getChar(s, 11, 10);
465             n += getChar(s, 12, 1);
466             minute = n;
467 
468             // sec
469             n = getChar(s, 13, 10);
470             n += getChar(s, 14, 1);
471             second = n;
472 
473             if (len > 15) {
474                 // Z
475                 checkChar(s, 15, 'Z');
476                 inUtc = true;
477             }
478         } else {
479             allDay = true;
480             hour = 0;
481             minute = 0;
482             second = 0;
483         }
484 
485         weekDay = 0;
486         yearDay = 0;
487         isDst = -1;
488         gmtoff = 0;
489         return inUtc;
490     }
491 
checkChar(String s, int spos, char expected)492     private void checkChar(String s, int spos, char expected) {
493         char c = s.charAt(spos);
494         if (c != expected) {
495             throw new TimeFormatException(String.format(
496                     "Unexpected character 0x%02d at pos=%d.  Expected 0x%02d (\'%c\').",
497                     (int) c, spos, (int) expected, expected));
498         }
499     }
500 
getChar(String s, int spos, int mul)501     private static int getChar(String s, int spos, int mul) {
502         char c = s.charAt(spos);
503         if (Character.isDigit(c)) {
504             return Character.getNumericValue(c) * mul;
505         } else {
506             throw new TimeFormatException("Parse error at pos=" + spos);
507         }
508     }
509 
510     /**
511      * Parse a time in RFC 3339 format.  This method also parses simple dates
512      * (that is, strings that contain no time or time offset).  For example,
513      * all of the following strings are valid:
514      *
515      * <ul>
516      *   <li>"2008-10-13T16:00:00.000Z"</li>
517      *   <li>"2008-10-13T16:00:00.000+07:00"</li>
518      *   <li>"2008-10-13T16:00:00.000-07:00"</li>
519      *   <li>"2008-10-13"</li>
520      * </ul>
521      *
522      * <p>
523      * If the string contains a time and time offset, then the time offset will
524      * be used to convert the time value to UTC.
525      * </p>
526      *
527      * <p>
528      * If the given string contains just a date (with no time field), then
529      * the {@link #allDay} field is set to true and the {@link #hour},
530      * {@link #minute}, and  {@link #second} fields are set to zero.
531      * </p>
532      *
533      * <p>
534      * Returns true if the resulting time value is in UTC time.
535      * </p>
536      *
537      * @param s the string to parse
538      * @return true if the resulting time value is in UTC time
539      * @throws android.util.TimeFormatException if s cannot be parsed.
540      */
parse3339(String s)541      public boolean parse3339(String s) {
542          if (s == null) {
543              throw new NullPointerException("time string is null");
544          }
545          if (parse3339Internal(s)) {
546              timezone = TIMEZONE_UTC;
547              return true;
548          }
549          return false;
550      }
551 
parse3339Internal(String s)552      private boolean parse3339Internal(String s) {
553          int len = s.length();
554          if (len < 10) {
555              throw new TimeFormatException("String too short --- expected at least 10 characters.");
556          }
557          boolean inUtc = false;
558 
559          // year
560          int n = getChar(s, 0, 1000);
561          n += getChar(s, 1, 100);
562          n += getChar(s, 2, 10);
563          n += getChar(s, 3, 1);
564          year = n;
565 
566          checkChar(s, 4, '-');
567 
568          // month
569          n = getChar(s, 5, 10);
570          n += getChar(s, 6, 1);
571          --n;
572          month = n;
573 
574          checkChar(s, 7, '-');
575 
576          // day
577          n = getChar(s, 8, 10);
578          n += getChar(s, 9, 1);
579          monthDay = n;
580 
581          if (len >= 19) {
582              // T
583              checkChar(s, 10, 'T');
584              allDay = false;
585 
586              // hour
587              n = getChar(s, 11, 10);
588              n += getChar(s, 12, 1);
589 
590              // Note that this.hour is not set here. It is set later.
591              int hour = n;
592 
593              checkChar(s, 13, ':');
594 
595              // minute
596              n = getChar(s, 14, 10);
597              n += getChar(s, 15, 1);
598              // Note that this.minute is not set here. It is set later.
599              int minute = n;
600 
601              checkChar(s, 16, ':');
602 
603              // second
604              n = getChar(s, 17, 10);
605              n += getChar(s, 18, 1);
606              second = n;
607 
608              // skip the '.XYZ' -- we don't care about subsecond precision.
609 
610              int tzIndex = 19;
611              if (tzIndex < len && s.charAt(tzIndex) == '.') {
612                  do {
613                      tzIndex++;
614                  } while (tzIndex < len && Character.isDigit(s.charAt(tzIndex)));
615              }
616 
617              int offset = 0;
618              if (len > tzIndex) {
619                  char c = s.charAt(tzIndex);
620                  // NOTE: the offset is meant to be subtracted to get from local time
621                  // to UTC.  we therefore use 1 for '-' and -1 for '+'.
622                  switch (c) {
623                      case 'Z':
624                          // Zulu time -- UTC
625                          offset = 0;
626                          break;
627                      case '-':
628                          offset = 1;
629                          break;
630                      case '+':
631                          offset = -1;
632                          break;
633                      default:
634                          throw new TimeFormatException(String.format(
635                                  "Unexpected character 0x%02d at position %d.  Expected + or -",
636                                  (int) c, tzIndex));
637                  }
638                  inUtc = true;
639 
640                  if (offset != 0) {
641                      if (len < tzIndex + 6) {
642                          throw new TimeFormatException(
643                                  String.format("Unexpected length; should be %d characters",
644                                          tzIndex + 6));
645                      }
646 
647                      // hour
648                      n = getChar(s, tzIndex + 1, 10);
649                      n += getChar(s, tzIndex + 2, 1);
650                      n *= offset;
651                      hour += n;
652 
653                      // minute
654                      n = getChar(s, tzIndex + 4, 10);
655                      n += getChar(s, tzIndex + 5, 1);
656                      n *= offset;
657                      minute += n;
658                  }
659              }
660              this.hour = hour;
661              this.minute = minute;
662 
663              if (offset != 0) {
664                  normalize(false);
665              }
666          } else {
667              allDay = true;
668              this.hour = 0;
669              this.minute = 0;
670              this.second = 0;
671          }
672 
673          this.weekDay = 0;
674          this.yearDay = 0;
675          this.isDst = -1;
676          this.gmtoff = 0;
677          return inUtc;
678      }
679 
680     /**
681      * Returns the timezone string that is currently set for the device.
682      */
getCurrentTimezone()683     public static String getCurrentTimezone() {
684         return TimeZone.getDefault().getID();
685     }
686 
687     /**
688      * Sets the time of the given Time object to the current time.
689      */
setToNow()690     public void setToNow() {
691         set(System.currentTimeMillis());
692     }
693 
694     /**
695      * Converts this time to milliseconds. Suitable for interacting with the
696      * standard java libraries. The time is in UTC milliseconds since the epoch.
697      * This does an implicit normalization to compute the milliseconds but does
698      * <em>not</em> change any of the fields in this Time object.  If you want
699      * to normalize the fields in this Time object and also get the milliseconds
700      * then use {@link #normalize(boolean)}.
701      *
702      * <p>
703      * If "ignoreDst" is false, then this method uses the current setting of the
704      * "isDst" field and will adjust the returned time if the "isDst" field is
705      * wrong for the given time.  See the sample code below for an example of
706      * this.
707      *
708      * <p>
709      * If "ignoreDst" is true, then this method ignores the current setting of
710      * the "isDst" field in this Time object and will instead figure out the
711      * correct value of "isDst" (as best it can) from the fields in this
712      * Time object.  The only case where this method cannot figure out the
713      * correct value of the "isDst" field is when the time is inherently
714      * ambiguous because it falls in the hour that is repeated when switching
715      * from Daylight-Saving Time to Standard Time.
716      *
717      * <p>
718      * Here is an example where <tt>toMillis(true)</tt> adjusts the time,
719      * assuming that DST changes at 2am on Sunday, Nov 4, 2007.
720      *
721      * <pre>
722      * Time time = new Time();
723      * time.set(4, 10, 2007);  // set the date to Nov 4, 2007, 12am
724      * time.normalize(false);       // this sets isDst = 1
725      * time.monthDay += 1;     // changes the date to Nov 5, 2007, 12am
726      * millis = time.toMillis(false);   // millis is Nov 4, 2007, 11pm
727      * millis = time.toMillis(true);    // millis is Nov 5, 2007, 12am
728      * </pre>
729      *
730      * <p>
731      * To avoid this problem, use <tt>toMillis(true)</tt>
732      * after adding or subtracting days or explicitly setting the "monthDay"
733      * field.  On the other hand, if you are adding
734      * or subtracting hours or minutes, then you should use
735      * <tt>toMillis(false)</tt>.
736      *
737      * <p>
738      * You should also use <tt>toMillis(false)</tt> if you want
739      * to read back the same milliseconds that you set with {@link #set(long)}
740      * or {@link #set(Time)} or after parsing a date string.
741      *
742      * <p>
743      * This method can return {@code -1} when the date / time fields have been
744      * set to a local time that conflicts with available timezone information.
745      * For example, when daylight savings transitions cause an hour to be
746      * skipped: times within that hour will return {@code -1} if isDst =
747      * {@code -1}.
748      */
toMillis(boolean ignoreDst)749     public long toMillis(boolean ignoreDst) {
750         calculator.copyFieldsFromTime(this);
751         return calculator.toMillis(ignoreDst);
752     }
753 
754     /**
755      * Sets the fields in this Time object given the UTC milliseconds.  After
756      * this method returns, all the fields are normalized.
757      * This also sets the "isDst" field to the correct value.
758      *
759      * @param millis the time in UTC milliseconds since the epoch.
760      */
set(long millis)761     public void set(long millis) {
762         allDay = false;
763         calculator.timezone = timezone;
764         calculator.setTimeInMillis(millis);
765         calculator.copyFieldsToTime(this);
766     }
767 
768     /**
769      * Format according to RFC 2445 DATE-TIME type.
770      *
771      * <p>The same as format("%Y%m%dT%H%M%S"), or format("%Y%m%dT%H%M%SZ") for a Time with a
772      * timezone set to "UTC".
773      */
format2445()774     public String format2445() {
775         calculator.copyFieldsFromTime(this);
776         return calculator.format2445(!allDay);
777     }
778 
779     /**
780      * Copy the value of that to this Time object. No normalization happens.
781      */
set(Time that)782     public void set(Time that) {
783         this.timezone = that.timezone;
784         this.allDay = that.allDay;
785         this.second = that.second;
786         this.minute = that.minute;
787         this.hour = that.hour;
788         this.monthDay = that.monthDay;
789         this.month = that.month;
790         this.year = that.year;
791         this.weekDay = that.weekDay;
792         this.yearDay = that.yearDay;
793         this.isDst = that.isDst;
794         this.gmtoff = that.gmtoff;
795     }
796 
797     /**
798      * Sets the fields. Sets weekDay, yearDay and gmtoff to 0, and isDst to -1.
799      * Call {@link #normalize(boolean)} if you need those.
800      */
set(int second, int minute, int hour, int monthDay, int month, int year)801     public void set(int second, int minute, int hour, int monthDay, int month, int year) {
802         this.allDay = false;
803         this.second = second;
804         this.minute = minute;
805         this.hour = hour;
806         this.monthDay = monthDay;
807         this.month = month;
808         this.year = year;
809         this.weekDay = 0;
810         this.yearDay = 0;
811         this.isDst = -1;
812         this.gmtoff = 0;
813     }
814 
815     /**
816      * Sets the date from the given fields.  Also sets allDay to true.
817      * Sets weekDay, yearDay and gmtoff to 0, and isDst to -1.
818      * Call {@link #normalize(boolean)} if you need those.
819      *
820      * @param monthDay the day of the month (in the range [1,31])
821      * @param month the zero-based month number (in the range [0,11])
822      * @param year the year
823      */
set(int monthDay, int month, int year)824     public void set(int monthDay, int month, int year) {
825         this.allDay = true;
826         this.second = 0;
827         this.minute = 0;
828         this.hour = 0;
829         this.monthDay = monthDay;
830         this.month = month;
831         this.year = year;
832         this.weekDay = 0;
833         this.yearDay = 0;
834         this.isDst = -1;
835         this.gmtoff = 0;
836     }
837 
838     /**
839      * Returns true if the time represented by this Time object occurs before
840      * the given time.
841      *
842      * <p>
843      * Equivalent to {@code Time.compare(this, that) < 0}. See
844      * {@link #compare(Time, Time)} for details.
845      *
846      * @param that a given Time object to compare against
847      * @return true if this time is less than the given time
848      */
before(Time that)849     public boolean before(Time that) {
850         return Time.compare(this, that) < 0;
851     }
852 
853 
854     /**
855      * Returns true if the time represented by this Time object occurs after
856      * the given time.
857      *
858      * <p>
859      * Equivalent to {@code Time.compare(this, that) > 0}. See
860      * {@link #compare(Time, Time)} for details.
861      *
862      * @param that a given Time object to compare against
863      * @return true if this time is greater than the given time
864      */
after(Time that)865     public boolean after(Time that) {
866         return Time.compare(this, that) > 0;
867     }
868 
869     /**
870      * This array is indexed by the weekDay field (SUNDAY=0, MONDAY=1, etc.)
871      * and gives a number that can be added to the yearDay to give the
872      * closest Thursday yearDay.
873      */
874     private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 };
875 
876     /**
877      * Computes the week number according to ISO 8601.  The current Time
878      * object must already be normalized because this method uses the
879      * yearDay and weekDay fields.
880      *
881      * <p>
882      * In IS0 8601, weeks start on Monday.
883      * The first week of the year (week 1) is defined by ISO 8601 as the
884      * first week with four or more of its days in the starting year.
885      * Or equivalently, the week containing January 4.  Or equivalently,
886      * the week with the year's first Thursday in it.
887      * </p>
888      *
889      * <p>
890      * The week number can be calculated by counting Thursdays.  Week N
891      * contains the Nth Thursday of the year.
892      * </p>
893      *
894      * @return the ISO week number.
895      */
getWeekNumber()896     public int getWeekNumber() {
897         // Get the year day for the closest Thursday
898         int closestThursday = yearDay + sThursdayOffset[weekDay];
899 
900         // Year days start at 0
901         if (closestThursday >= 0 && closestThursday <= 364) {
902             return closestThursday / 7 + 1;
903         }
904 
905         // The week crosses a year boundary.
906         Time temp = new Time(this);
907         temp.monthDay += sThursdayOffset[weekDay];
908         temp.normalize(true /* ignore isDst */);
909         return temp.yearDay / 7 + 1;
910     }
911 
912     /**
913      * Return a string in the RFC 3339 format.
914      * <p>
915      * If allDay is true, expresses the time as Y-M-D</p>
916      * <p>
917      * Otherwise, if the timezone is UTC, expresses the time as Y-M-D-T-H-M-S UTC</p>
918      * <p>
919      * Otherwise the time is expressed the time as Y-M-D-T-H-M-S +- GMT</p>
920      * @return string in the RFC 3339 format.
921      */
format3339(boolean allDay)922     public String format3339(boolean allDay) {
923         if (allDay) {
924             return format(Y_M_D);
925         } else if (TIMEZONE_UTC.equals(timezone)) {
926             return format(Y_M_D_T_H_M_S_000_Z);
927         } else {
928             String base = format(Y_M_D_T_H_M_S_000);
929             String sign = (gmtoff < 0) ? "-" : "+";
930             int offset = (int) Math.abs(gmtoff);
931             int minutes = (offset % 3600) / 60;
932             int hours = offset / 3600;
933 
934             return String.format(Locale.US, "%s%s%02d:%02d", base, sign, hours, minutes);
935         }
936     }
937 
938     /**
939      * Returns true if the day of the given time is the epoch on the Julian Calendar
940      * (January 1, 1970 on the Gregorian calendar).
941      *
942      * <p>
943      * This method can return an incorrect answer when the date / time fields have
944      * been set to a local time that contradicts the available timezone information.
945      *
946      * @param time the time to test
947      * @return true if epoch.
948      */
isEpoch(Time time)949     public static boolean isEpoch(Time time) {
950         long millis = time.toMillis(true);
951         return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY;
952     }
953 
954     /**
955      * Computes the Julian day number for a point in time in a particular
956      * timezone. The Julian day for a given date is the same for every
957      * timezone. For example, the Julian day for July 1, 2008 is 2454649.
958      *
959      * <p>Callers must pass the time in UTC millisecond (as can be returned
960      * by {@link #toMillis(boolean)} or {@link #normalize(boolean)})
961      * and the offset from UTC of the timezone in seconds (as might be in
962      * {@link #gmtoff}).
963      *
964      * <p>The Julian day is useful for testing if two events occur on the
965      * same calendar date and for determining the relative time of an event
966      * from the present ("yesterday", "3 days ago", etc.).
967      *
968      * @param millis the time in UTC milliseconds
969      * @param gmtoff the offset from UTC in seconds
970      * @return the Julian day
971      */
getJulianDay(long millis, long gmtoff)972     public static int getJulianDay(long millis, long gmtoff) {
973         long offsetMillis = gmtoff * 1000;
974         long julianDay = (millis + offsetMillis) / DateUtils.DAY_IN_MILLIS;
975         return (int) julianDay + EPOCH_JULIAN_DAY;
976     }
977 
978     /**
979      * <p>Sets the time from the given Julian day number, which must be based on
980      * the same timezone that is set in this Time object.  The "gmtoff" field
981      * need not be initialized because the given Julian day may have a different
982      * GMT offset than whatever is currently stored in this Time object anyway.
983      * After this method returns all the fields will be normalized and the time
984      * will be set to 12am at the beginning of the given Julian day.
985      * </p>
986      *
987      * <p>
988      * The only exception to this is if 12am does not exist for that day because
989      * of daylight saving time.  For example, Cairo, Eqypt moves time ahead one
990      * hour at 12am on April 25, 2008 and there are a few other places that
991      * also change daylight saving time at 12am.  In those cases, the time
992      * will be set to 1am.
993      * </p>
994      *
995      * @param julianDay the Julian day in the timezone for this Time object
996      * @return the UTC milliseconds for the beginning of the Julian day
997      */
setJulianDay(int julianDay)998     public long setJulianDay(int julianDay) {
999         // Don't bother with the GMT offset since we don't know the correct
1000         // value for the given Julian day.  Just get close and then adjust
1001         // the day.
1002         long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS;
1003         set(millis);
1004 
1005         // Figure out how close we are to the requested Julian day.
1006         // We can't be off by more than a day.
1007         int approximateDay = getJulianDay(millis, gmtoff);
1008         int diff = julianDay - approximateDay;
1009         monthDay += diff;
1010 
1011         // Set the time to 12am and re-normalize.
1012         hour = 0;
1013         minute = 0;
1014         second = 0;
1015         millis = normalize(true);
1016         return millis;
1017     }
1018 
1019     /**
1020      * Returns the week since {@link #EPOCH_JULIAN_DAY} (Jan 1, 1970) adjusted
1021      * for first day of week. This takes a julian day and the week start day and
1022      * calculates which week since {@link #EPOCH_JULIAN_DAY} that day occurs in,
1023      * starting at 0. *Do not* use this to compute the ISO week number for the
1024      * year.
1025      *
1026      * @param julianDay The julian day to calculate the week number for
1027      * @param firstDayOfWeek Which week day is the first day of the week, see
1028      *            {@link #SUNDAY}
1029      * @return Weeks since the epoch
1030      */
getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek)1031     public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
1032         int diff = THURSDAY - firstDayOfWeek;
1033         if (diff < 0) {
1034             diff += 7;
1035         }
1036         int refDay = EPOCH_JULIAN_DAY - diff;
1037         return (julianDay - refDay) / 7;
1038     }
1039 
1040     /**
1041      * Takes a number of weeks since the epoch and calculates the Julian day of
1042      * the Monday for that week. This assumes that the week containing the
1043      * {@link #EPOCH_JULIAN_DAY} is considered week 0. It returns the Julian day
1044      * for the Monday week weeks after the Monday of the week containing the
1045      * epoch.
1046      *
1047      * @param week Number of weeks since the epoch
1048      * @return The julian day for the Monday of the given week since the epoch
1049      */
getJulianMondayFromWeeksSinceEpoch(int week)1050     public static int getJulianMondayFromWeeksSinceEpoch(int week) {
1051         return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
1052     }
1053 
1054     /**
1055      * A class that handles date/time calculations.
1056      *
1057      * This class originated as a port of a native C++ class ("android.Time") to pure Java. It is
1058      * separate from the enclosing class because some methods copy the result of calculations back
1059      * to the enclosing object, but others do not: thus separate state is retained.
1060      */
1061     private static class TimeCalculator {
1062         public final ZoneInfoData.WallTime wallTime;
1063         public String timezone;
1064 
1065         // Information about the current timezone.
1066         private ZoneInfoData mZoneInfoData;
1067 
TimeCalculator(String timezoneId)1068         public TimeCalculator(String timezoneId) {
1069             this.mZoneInfoData = lookupZoneInfoData(timezoneId);
1070             this.wallTime = new ZoneInfoData.WallTime();
1071         }
1072 
toMillis(boolean ignoreDst)1073         public long toMillis(boolean ignoreDst) {
1074             if (ignoreDst) {
1075                 wallTime.setIsDst(-1);
1076             }
1077 
1078             int r = wallTime.mktime(mZoneInfoData);
1079             if (r == -1) {
1080                 return -1;
1081             }
1082             return r * 1000L;
1083         }
1084 
setTimeInMillis(long millis)1085         public void setTimeInMillis(long millis) {
1086             // Preserve old 32-bit Android behavior.
1087             int intSeconds = (int) (millis / 1000);
1088 
1089             updateZoneInfoFromTimeZone();
1090             wallTime.localtime(intSeconds, mZoneInfoData);
1091         }
1092 
format(String format)1093         public String format(String format) {
1094             if (format == null) {
1095                 format = "%c";
1096             }
1097             TimeFormatter formatter = new TimeFormatter();
1098             return formatter.format(format, wallTime, mZoneInfoData);
1099         }
1100 
updateZoneInfoFromTimeZone()1101         private void updateZoneInfoFromTimeZone() {
1102             if (!mZoneInfoData.getID().equals(timezone)) {
1103                 this.mZoneInfoData = lookupZoneInfoData(timezone);
1104             }
1105         }
1106 
lookupZoneInfoData(String timezoneId)1107         private static ZoneInfoData lookupZoneInfoData(String timezoneId) {
1108             ZoneInfoData zoneInfoData = ZoneInfoDb.getInstance().makeZoneInfoData(timezoneId);
1109             if (zoneInfoData == null) {
1110                 zoneInfoData = ZoneInfoDb.getInstance().makeZoneInfoData("GMT");
1111             }
1112             if (zoneInfoData == null) {
1113                 throw new AssertionError("GMT not found: \"" + timezoneId + "\"");
1114             }
1115             return zoneInfoData;
1116         }
1117 
switchTimeZone(String timezone)1118         public void switchTimeZone(String timezone) {
1119             int seconds = wallTime.mktime(mZoneInfoData);
1120             this.timezone = timezone;
1121             updateZoneInfoFromTimeZone();
1122             wallTime.localtime(seconds, mZoneInfoData);
1123         }
1124 
format2445(boolean hasTime)1125         public String format2445(boolean hasTime) {
1126             char[] buf = new char[hasTime ? 16 : 8];
1127             int n = wallTime.getYear();
1128 
1129             buf[0] = toChar(n / 1000);
1130             n %= 1000;
1131             buf[1] = toChar(n / 100);
1132             n %= 100;
1133             buf[2] = toChar(n / 10);
1134             n %= 10;
1135             buf[3] = toChar(n);
1136 
1137             n = wallTime.getMonth() + 1;
1138             buf[4] = toChar(n / 10);
1139             buf[5] = toChar(n % 10);
1140 
1141             n = wallTime.getMonthDay();
1142             buf[6] = toChar(n / 10);
1143             buf[7] = toChar(n % 10);
1144 
1145             if (!hasTime) {
1146                 return new String(buf, 0, 8);
1147             }
1148 
1149             buf[8] = 'T';
1150 
1151             n = wallTime.getHour();
1152             buf[9] = toChar(n / 10);
1153             buf[10] = toChar(n % 10);
1154 
1155             n = wallTime.getMinute();
1156             buf[11] = toChar(n / 10);
1157             buf[12] = toChar(n % 10);
1158 
1159             n = wallTime.getSecond();
1160             buf[13] = toChar(n / 10);
1161             buf[14] = toChar(n % 10);
1162 
1163             if (TIMEZONE_UTC.equals(timezone)) {
1164                 // The letter 'Z' is appended to the end.
1165                 buf[15] = 'Z';
1166                 return new String(buf, 0, 16);
1167             } else {
1168                 return new String(buf, 0, 15);
1169             }
1170         }
1171 
toChar(int n)1172         private char toChar(int n) {
1173             return (n >= 0 && n <= 9) ? (char) (n + '0') : ' ';
1174         }
1175 
1176         /**
1177          * A method that will return the state of this object in string form. Note: it has side
1178          * effects and so has deliberately not been made the default {@link #toString()}.
1179          */
toStringInternal()1180         public String toStringInternal() {
1181             // This implementation possibly displays the un-normalized fields because that is
1182             // what it has always done.
1183             return String.format("%04d%02d%02dT%02d%02d%02d%s(%d,%d,%d,%d,%d)",
1184                     wallTime.getYear(),
1185                     wallTime.getMonth() + 1,
1186                     wallTime.getMonthDay(),
1187                     wallTime.getHour(),
1188                     wallTime.getMinute(),
1189                     wallTime.getSecond(),
1190                     timezone,
1191                     wallTime.getWeekDay(),
1192                     wallTime.getYearDay(),
1193                     wallTime.getGmtOffset(),
1194                     wallTime.getIsDst(),
1195                     toMillis(false /* use isDst */) / 1000
1196             );
1197 
1198         }
1199 
compare(TimeCalculator aObject, TimeCalculator bObject)1200         public static int compare(TimeCalculator aObject, TimeCalculator bObject) {
1201             if (aObject.timezone.equals(bObject.timezone)) {
1202                 // If the timezones are the same, we can easily compare the two times.
1203                 int diff = aObject.wallTime.getYear() - bObject.wallTime.getYear();
1204                 if (diff != 0) {
1205                     return diff;
1206                 }
1207 
1208                 diff = aObject.wallTime.getMonth() - bObject.wallTime.getMonth();
1209                 if (diff != 0) {
1210                     return diff;
1211                 }
1212 
1213                 diff = aObject.wallTime.getMonthDay() - bObject.wallTime.getMonthDay();
1214                 if (diff != 0) {
1215                     return diff;
1216                 }
1217 
1218                 diff = aObject.wallTime.getHour() - bObject.wallTime.getHour();
1219                 if (diff != 0) {
1220                     return diff;
1221                 }
1222 
1223                 diff = aObject.wallTime.getMinute() - bObject.wallTime.getMinute();
1224                 if (diff != 0) {
1225                     return diff;
1226                 }
1227 
1228                 diff = aObject.wallTime.getSecond() - bObject.wallTime.getSecond();
1229                 if (diff != 0) {
1230                     return diff;
1231                 }
1232 
1233                 return 0;
1234             } else {
1235                 // Otherwise, convert to milliseconds and compare that. This requires that object be
1236                 // normalized. Note: For dates that do not exist: toMillis() can return -1, which
1237                 // can be confused with a valid time.
1238                 long am = aObject.toMillis(false /* use isDst */);
1239                 long bm = bObject.toMillis(false /* use isDst */);
1240                 long diff = am - bm;
1241                 return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0);
1242             }
1243 
1244         }
1245 
copyFieldsToTime(Time time)1246         public void copyFieldsToTime(Time time) {
1247             time.second = wallTime.getSecond();
1248             time.minute = wallTime.getMinute();
1249             time.hour = wallTime.getHour();
1250             time.monthDay = wallTime.getMonthDay();
1251             time.month = wallTime.getMonth();
1252             time.year = wallTime.getYear();
1253 
1254             // Read-only fields that are derived from other information above.
1255             time.weekDay = wallTime.getWeekDay();
1256             time.yearDay = wallTime.getYearDay();
1257 
1258             // < 0: DST status unknown, 0: is not in DST, 1: is in DST
1259             time.isDst = wallTime.getIsDst();
1260             // This is in seconds and includes any DST offset too.
1261             time.gmtoff = wallTime.getGmtOffset();
1262         }
1263 
copyFieldsFromTime(Time time)1264         public void copyFieldsFromTime(Time time) {
1265             wallTime.setSecond(time.second);
1266             wallTime.setMinute(time.minute);
1267             wallTime.setHour(time.hour);
1268             wallTime.setMonthDay(time.monthDay);
1269             wallTime.setMonth(time.month);
1270             wallTime.setYear(time.year);
1271             wallTime.setWeekDay(time.weekDay);
1272             wallTime.setYearDay(time.yearDay);
1273             wallTime.setIsDst(time.isDst);
1274             wallTime.setGmtOffset((int) time.gmtoff);
1275 
1276             if (time.allDay && (time.second != 0 || time.minute != 0 || time.hour != 0)) {
1277                 throw new IllegalArgumentException("allDay is true but sec, min, hour are not 0.");
1278             }
1279 
1280             timezone = time.timezone;
1281             updateZoneInfoFromTimeZone();
1282         }
1283     }
1284 }
1285