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