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