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