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