• 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 libcore.timezone.ZoneInfoDB;
22 import libcore.util.ZoneInfo;
23 
24 import java.io.IOException;
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 man
350      * strftime for what means what. The final string must be less than 256
351      * characters.
352      * @param format a string containing the desired format.
353      * @return a String containing the current time expressed in the current locale.
354      */
format(String format)355     public String format(String format) {
356         calculator.copyFieldsFromTime(this);
357         return calculator.format(format);
358     }
359 
360     /**
361      * Return the current time in YYYYMMDDTHHMMSS&lt;tz&gt; format
362      */
363     @Override
toString()364     public String toString() {
365         // toString() uses its own TimeCalculator rather than the shared one. Otherwise crazy stuff
366         // happens during debugging when the debugger calls toString().
367         TimeCalculator calculator = new TimeCalculator(this.timezone);
368         calculator.copyFieldsFromTime(this);
369         return calculator.toStringInternal();
370     }
371 
372     /**
373      * Parses a date-time string in either the RFC 2445 format or an abbreviated
374      * format that does not include the "time" field.  For example, all of the
375      * following strings are valid:
376      *
377      * <ul>
378      *   <li>"20081013T160000Z"</li>
379      *   <li>"20081013T160000"</li>
380      *   <li>"20081013"</li>
381      * </ul>
382      *
383      * Returns whether or not the time is in UTC (ends with Z).  If the string
384      * ends with "Z" then the timezone is set to UTC.  If the date-time string
385      * included only a date and no time field, then the <code>allDay</code>
386      * field of this Time class is set to true and the <code>hour</code>,
387      * <code>minute</code>, and <code>second</code> fields are set to zero;
388      * otherwise (a time field was included in the date-time string)
389      * <code>allDay</code> is set to false. The fields <code>weekDay</code>,
390      * <code>yearDay</code>, and <code>gmtoff</code> are always set to zero,
391      * and the field <code>isDst</code> is set to -1 (unknown).  To set those
392      * fields, call {@link #normalize(boolean)} after parsing.
393      *
394      * To parse a date-time string and convert it to UTC milliseconds, do
395      * something like this:
396      *
397      * <pre>
398      *   Time time = new Time();
399      *   String date = "20081013T160000Z";
400      *   time.parse(date);
401      *   long millis = time.normalize(false);
402      * </pre>
403      *
404      * @param s the string to parse
405      * @return true if the resulting time value is in UTC time
406      * @throws android.util.TimeFormatException if s cannot be parsed.
407      */
parse(String s)408     public boolean parse(String s) {
409         if (s == null) {
410             throw new NullPointerException("time string is null");
411         }
412         if (parseInternal(s)) {
413             timezone = TIMEZONE_UTC;
414             return true;
415         }
416         return false;
417     }
418 
419     /**
420      * Parse a time in the current zone in YYYYMMDDTHHMMSS format.
421      */
parseInternal(String s)422     private boolean parseInternal(String s) {
423         int len = s.length();
424         if (len < 8) {
425             throw new TimeFormatException("String is too short: \"" + s +
426                     "\" Expected at least 8 characters.");
427         }
428 
429         boolean inUtc = false;
430 
431         // year
432         int n = getChar(s, 0, 1000);
433         n += getChar(s, 1, 100);
434         n += getChar(s, 2, 10);
435         n += getChar(s, 3, 1);
436         year = n;
437 
438         // month
439         n = getChar(s, 4, 10);
440         n += getChar(s, 5, 1);
441         n--;
442         month = n;
443 
444         // day of month
445         n = getChar(s, 6, 10);
446         n += getChar(s, 7, 1);
447         monthDay = n;
448 
449         if (len > 8) {
450             if (len < 15) {
451                 throw new TimeFormatException(
452                         "String is too short: \"" + s
453                                 + "\" If there are more than 8 characters there must be at least"
454                                 + " 15.");
455             }
456             checkChar(s, 8, 'T');
457             allDay = false;
458 
459             // hour
460             n = getChar(s, 9, 10);
461             n += getChar(s, 10, 1);
462             hour = n;
463 
464             // min
465             n = getChar(s, 11, 10);
466             n += getChar(s, 12, 1);
467             minute = n;
468 
469             // sec
470             n = getChar(s, 13, 10);
471             n += getChar(s, 14, 1);
472             second = n;
473 
474             if (len > 15) {
475                 // Z
476                 checkChar(s, 15, 'Z');
477                 inUtc = true;
478             }
479         } else {
480             allDay = true;
481             hour = 0;
482             minute = 0;
483             second = 0;
484         }
485 
486         weekDay = 0;
487         yearDay = 0;
488         isDst = -1;
489         gmtoff = 0;
490         return inUtc;
491     }
492 
checkChar(String s, int spos, char expected)493     private void checkChar(String s, int spos, char expected) {
494         char c = s.charAt(spos);
495         if (c != expected) {
496             throw new TimeFormatException(String.format(
497                     "Unexpected character 0x%02d at pos=%d.  Expected 0x%02d (\'%c\').",
498                     (int) c, spos, (int) expected, expected));
499         }
500     }
501 
getChar(String s, int spos, int mul)502     private static int getChar(String s, int spos, int mul) {
503         char c = s.charAt(spos);
504         if (Character.isDigit(c)) {
505             return Character.getNumericValue(c) * mul;
506         } else {
507             throw new TimeFormatException("Parse error at pos=" + spos);
508         }
509     }
510 
511     /**
512      * Parse a time in RFC 3339 format.  This method also parses simple dates
513      * (that is, strings that contain no time or time offset).  For example,
514      * all of the following strings are valid:
515      *
516      * <ul>
517      *   <li>"2008-10-13T16:00:00.000Z"</li>
518      *   <li>"2008-10-13T16:00:00.000+07:00"</li>
519      *   <li>"2008-10-13T16:00:00.000-07:00"</li>
520      *   <li>"2008-10-13"</li>
521      * </ul>
522      *
523      * <p>
524      * If the string contains a time and time offset, then the time offset will
525      * be used to convert the time value to UTC.
526      * </p>
527      *
528      * <p>
529      * If the given string contains just a date (with no time field), then
530      * the {@link #allDay} field is set to true and the {@link #hour},
531      * {@link #minute}, and  {@link #second} fields are set to zero.
532      * </p>
533      *
534      * <p>
535      * Returns true if the resulting time value is in UTC time.
536      * </p>
537      *
538      * @param s the string to parse
539      * @return true if the resulting time value is in UTC time
540      * @throws android.util.TimeFormatException if s cannot be parsed.
541      */
parse3339(String s)542      public boolean parse3339(String s) {
543          if (s == null) {
544              throw new NullPointerException("time string is null");
545          }
546          if (parse3339Internal(s)) {
547              timezone = TIMEZONE_UTC;
548              return true;
549          }
550          return false;
551      }
552 
parse3339Internal(String s)553      private boolean parse3339Internal(String s) {
554          int len = s.length();
555          if (len < 10) {
556              throw new TimeFormatException("String too short --- expected at least 10 characters.");
557          }
558          boolean inUtc = false;
559 
560          // year
561          int n = getChar(s, 0, 1000);
562          n += getChar(s, 1, 100);
563          n += getChar(s, 2, 10);
564          n += getChar(s, 3, 1);
565          year = n;
566 
567          checkChar(s, 4, '-');
568 
569          // month
570          n = getChar(s, 5, 10);
571          n += getChar(s, 6, 1);
572          --n;
573          month = n;
574 
575          checkChar(s, 7, '-');
576 
577          // day
578          n = getChar(s, 8, 10);
579          n += getChar(s, 9, 1);
580          monthDay = n;
581 
582          if (len >= 19) {
583              // T
584              checkChar(s, 10, 'T');
585              allDay = false;
586 
587              // hour
588              n = getChar(s, 11, 10);
589              n += getChar(s, 12, 1);
590 
591              // Note that this.hour is not set here. It is set later.
592              int hour = n;
593 
594              checkChar(s, 13, ':');
595 
596              // minute
597              n = getChar(s, 14, 10);
598              n += getChar(s, 15, 1);
599              // Note that this.minute is not set here. It is set later.
600              int minute = n;
601 
602              checkChar(s, 16, ':');
603 
604              // second
605              n = getChar(s, 17, 10);
606              n += getChar(s, 18, 1);
607              second = n;
608 
609              // skip the '.XYZ' -- we don't care about subsecond precision.
610 
611              int tzIndex = 19;
612              if (tzIndex < len && s.charAt(tzIndex) == '.') {
613                  do {
614                      tzIndex++;
615                  } while (tzIndex < len && Character.isDigit(s.charAt(tzIndex)));
616              }
617 
618              int offset = 0;
619              if (len > tzIndex) {
620                  char c = s.charAt(tzIndex);
621                  // NOTE: the offset is meant to be subtracted to get from local time
622                  // to UTC.  we therefore use 1 for '-' and -1 for '+'.
623                  switch (c) {
624                      case 'Z':
625                          // Zulu time -- UTC
626                          offset = 0;
627                          break;
628                      case '-':
629                          offset = 1;
630                          break;
631                      case '+':
632                          offset = -1;
633                          break;
634                      default:
635                          throw new TimeFormatException(String.format(
636                                  "Unexpected character 0x%02d at position %d.  Expected + or -",
637                                  (int) c, tzIndex));
638                  }
639                  inUtc = true;
640 
641                  if (offset != 0) {
642                      if (len < tzIndex + 6) {
643                          throw new TimeFormatException(
644                                  String.format("Unexpected length; should be %d characters",
645                                          tzIndex + 6));
646                      }
647 
648                      // hour
649                      n = getChar(s, tzIndex + 1, 10);
650                      n += getChar(s, tzIndex + 2, 1);
651                      n *= offset;
652                      hour += n;
653 
654                      // minute
655                      n = getChar(s, tzIndex + 4, 10);
656                      n += getChar(s, tzIndex + 5, 1);
657                      n *= offset;
658                      minute += n;
659                  }
660              }
661              this.hour = hour;
662              this.minute = minute;
663 
664              if (offset != 0) {
665                  normalize(false);
666              }
667          } else {
668              allDay = true;
669              this.hour = 0;
670              this.minute = 0;
671              this.second = 0;
672          }
673 
674          this.weekDay = 0;
675          this.yearDay = 0;
676          this.isDst = -1;
677          this.gmtoff = 0;
678          return inUtc;
679      }
680 
681     /**
682      * Returns the timezone string that is currently set for the device.
683      */
getCurrentTimezone()684     public static String getCurrentTimezone() {
685         return TimeZone.getDefault().getID();
686     }
687 
688     /**
689      * Sets the time of the given Time object to the current time.
690      */
setToNow()691     public void setToNow() {
692         set(System.currentTimeMillis());
693     }
694 
695     /**
696      * Converts this time to milliseconds. Suitable for interacting with the
697      * standard java libraries. The time is in UTC milliseconds since the epoch.
698      * This does an implicit normalization to compute the milliseconds but does
699      * <em>not</em> change any of the fields in this Time object.  If you want
700      * to normalize the fields in this Time object and also get the milliseconds
701      * then use {@link #normalize(boolean)}.
702      *
703      * <p>
704      * If "ignoreDst" is false, then this method uses the current setting of the
705      * "isDst" field and will adjust the returned time if the "isDst" field is
706      * wrong for the given time.  See the sample code below for an example of
707      * this.
708      *
709      * <p>
710      * If "ignoreDst" is true, then this method ignores the current setting of
711      * the "isDst" field in this Time object and will instead figure out the
712      * correct value of "isDst" (as best it can) from the fields in this
713      * Time object.  The only case where this method cannot figure out the
714      * correct value of the "isDst" field is when the time is inherently
715      * ambiguous because it falls in the hour that is repeated when switching
716      * from Daylight-Saving Time to Standard Time.
717      *
718      * <p>
719      * Here is an example where <tt>toMillis(true)</tt> adjusts the time,
720      * assuming that DST changes at 2am on Sunday, Nov 4, 2007.
721      *
722      * <pre>
723      * Time time = new Time();
724      * time.set(4, 10, 2007);  // set the date to Nov 4, 2007, 12am
725      * time.normalize(false);       // this sets isDst = 1
726      * time.monthDay += 1;     // changes the date to Nov 5, 2007, 12am
727      * millis = time.toMillis(false);   // millis is Nov 4, 2007, 11pm
728      * millis = time.toMillis(true);    // millis is Nov 5, 2007, 12am
729      * </pre>
730      *
731      * <p>
732      * To avoid this problem, use <tt>toMillis(true)</tt>
733      * after adding or subtracting days or explicitly setting the "monthDay"
734      * field.  On the other hand, if you are adding
735      * or subtracting hours or minutes, then you should use
736      * <tt>toMillis(false)</tt>.
737      *
738      * <p>
739      * You should also use <tt>toMillis(false)</tt> if you want
740      * to read back the same milliseconds that you set with {@link #set(long)}
741      * 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 day of the given time is the epoch on the Julian Calendar
941      * (January 1, 1970 on the Gregorian calendar).
942      *
943      * <p>
944      * This method can return an incorrect answer when the date / time fields have
945      * been set to a local time that contradicts the available timezone information.
946      *
947      * @param time the time to test
948      * @return true if epoch.
949      */
isEpoch(Time time)950     public static boolean isEpoch(Time time) {
951         long millis = time.toMillis(true);
952         return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY;
953     }
954 
955     /**
956      * Computes the Julian day number for a point in time in a particular
957      * timezone. The Julian day for a given date is the same for every
958      * timezone. For example, the Julian day for July 1, 2008 is 2454649.
959      *
960      * <p>Callers must pass the time in UTC millisecond (as can be returned
961      * by {@link #toMillis(boolean)} or {@link #normalize(boolean)})
962      * and the offset from UTC of the timezone in seconds (as might be in
963      * {@link #gmtoff}).
964      *
965      * <p>The Julian day is useful for testing if two events occur on the
966      * same calendar date and for determining the relative time of an event
967      * from the present ("yesterday", "3 days ago", etc.).
968      *
969      * @param millis the time in UTC milliseconds
970      * @param gmtoff the offset from UTC in seconds
971      * @return the Julian day
972      */
getJulianDay(long millis, long gmtoff)973     public static int getJulianDay(long millis, long gmtoff) {
974         long offsetMillis = gmtoff * 1000;
975         long julianDay = (millis + offsetMillis) / DateUtils.DAY_IN_MILLIS;
976         return (int) julianDay + EPOCH_JULIAN_DAY;
977     }
978 
979     /**
980      * <p>Sets the time from the given Julian day number, which must be based on
981      * the same timezone that is set in this Time object.  The "gmtoff" field
982      * need not be initialized because the given Julian day may have a different
983      * GMT offset than whatever is currently stored in this Time object anyway.
984      * After this method returns all the fields will be normalized and the time
985      * will be set to 12am at the beginning of the given Julian day.
986      * </p>
987      *
988      * <p>
989      * The only exception to this is if 12am does not exist for that day because
990      * of daylight saving time.  For example, Cairo, Eqypt moves time ahead one
991      * hour at 12am on April 25, 2008 and there are a few other places that
992      * also change daylight saving time at 12am.  In those cases, the time
993      * will be set to 1am.
994      * </p>
995      *
996      * @param julianDay the Julian day in the timezone for this Time object
997      * @return the UTC milliseconds for the beginning of the Julian day
998      */
setJulianDay(int julianDay)999     public long setJulianDay(int julianDay) {
1000         // Don't bother with the GMT offset since we don't know the correct
1001         // value for the given Julian day.  Just get close and then adjust
1002         // the day.
1003         long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS;
1004         set(millis);
1005 
1006         // Figure out how close we are to the requested Julian day.
1007         // We can't be off by more than a day.
1008         int approximateDay = getJulianDay(millis, gmtoff);
1009         int diff = julianDay - approximateDay;
1010         monthDay += diff;
1011 
1012         // Set the time to 12am and re-normalize.
1013         hour = 0;
1014         minute = 0;
1015         second = 0;
1016         millis = normalize(true);
1017         return millis;
1018     }
1019 
1020     /**
1021      * Returns the week since {@link #EPOCH_JULIAN_DAY} (Jan 1, 1970) adjusted
1022      * for first day of week. This takes a julian day and the week start day and
1023      * calculates which week since {@link #EPOCH_JULIAN_DAY} that day occurs in,
1024      * starting at 0. *Do not* use this to compute the ISO week number for the
1025      * year.
1026      *
1027      * @param julianDay The julian day to calculate the week number for
1028      * @param firstDayOfWeek Which week day is the first day of the week, see
1029      *            {@link #SUNDAY}
1030      * @return Weeks since the epoch
1031      */
getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek)1032     public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
1033         int diff = THURSDAY - firstDayOfWeek;
1034         if (diff < 0) {
1035             diff += 7;
1036         }
1037         int refDay = EPOCH_JULIAN_DAY - diff;
1038         return (julianDay - refDay) / 7;
1039     }
1040 
1041     /**
1042      * Takes a number of weeks since the epoch and calculates the Julian day of
1043      * the Monday for that week. This assumes that the week containing the
1044      * {@link #EPOCH_JULIAN_DAY} is considered week 0. It returns the Julian day
1045      * for the Monday week weeks after the Monday of the week containing the
1046      * epoch.
1047      *
1048      * @param week Number of weeks since the epoch
1049      * @return The julian day for the Monday of the given week since the epoch
1050      */
getJulianMondayFromWeeksSinceEpoch(int week)1051     public static int getJulianMondayFromWeeksSinceEpoch(int week) {
1052         return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
1053     }
1054 
1055     /**
1056      * A class that handles date/time calculations.
1057      *
1058      * This class originated as a port of a native C++ class ("android.Time") to pure Java. It is
1059      * separate from the enclosing class because some methods copy the result of calculations back
1060      * to the enclosing object, but others do not: thus separate state is retained.
1061      */
1062     private static class TimeCalculator {
1063         public final ZoneInfo.WallTime wallTime;
1064         public String timezone;
1065 
1066         // Information about the current timezone.
1067         private ZoneInfo zoneInfo;
1068 
TimeCalculator(String timezoneId)1069         public TimeCalculator(String timezoneId) {
1070             this.zoneInfo = lookupZoneInfo(timezoneId);
1071             this.wallTime = new ZoneInfo.WallTime();
1072         }
1073 
toMillis(boolean ignoreDst)1074         public long toMillis(boolean ignoreDst) {
1075             if (ignoreDst) {
1076                 wallTime.setIsDst(-1);
1077             }
1078 
1079             int r = wallTime.mktime(zoneInfo);
1080             if (r == -1) {
1081                 return -1;
1082             }
1083             return r * 1000L;
1084         }
1085 
setTimeInMillis(long millis)1086         public void setTimeInMillis(long millis) {
1087             // Preserve old 32-bit Android behavior.
1088             int intSeconds = (int) (millis / 1000);
1089 
1090             updateZoneInfoFromTimeZone();
1091             wallTime.localtime(intSeconds, zoneInfo);
1092         }
1093 
format(String format)1094         public String format(String format) {
1095             if (format == null) {
1096                 format = "%c";
1097             }
1098             TimeFormatter formatter = new TimeFormatter();
1099             return formatter.format(format, wallTime, zoneInfo);
1100         }
1101 
updateZoneInfoFromTimeZone()1102         private void updateZoneInfoFromTimeZone() {
1103             if (!zoneInfo.getID().equals(timezone)) {
1104                 this.zoneInfo = lookupZoneInfo(timezone);
1105             }
1106         }
1107 
lookupZoneInfo(String timezoneId)1108         private static ZoneInfo lookupZoneInfo(String timezoneId) {
1109             try {
1110                 ZoneInfo zoneInfo = ZoneInfoDB.getInstance().makeTimeZone(timezoneId);
1111                 if (zoneInfo == null) {
1112                     zoneInfo = ZoneInfoDB.getInstance().makeTimeZone("GMT");
1113                 }
1114                 if (zoneInfo == null) {
1115                     throw new AssertionError("GMT not found: \"" + timezoneId + "\"");
1116                 }
1117                 return zoneInfo;
1118             } catch (IOException e) {
1119                 // This should not ever be thrown.
1120                 throw new AssertionError("Error loading timezone: \"" + timezoneId + "\"", e);
1121             }
1122         }
1123 
switchTimeZone(String timezone)1124         public void switchTimeZone(String timezone) {
1125             int seconds = wallTime.mktime(zoneInfo);
1126             this.timezone = timezone;
1127             updateZoneInfoFromTimeZone();
1128             wallTime.localtime(seconds, zoneInfo);
1129         }
1130 
format2445(boolean hasTime)1131         public String format2445(boolean hasTime) {
1132             char[] buf = new char[hasTime ? 16 : 8];
1133             int n = wallTime.getYear();
1134 
1135             buf[0] = toChar(n / 1000);
1136             n %= 1000;
1137             buf[1] = toChar(n / 100);
1138             n %= 100;
1139             buf[2] = toChar(n / 10);
1140             n %= 10;
1141             buf[3] = toChar(n);
1142 
1143             n = wallTime.getMonth() + 1;
1144             buf[4] = toChar(n / 10);
1145             buf[5] = toChar(n % 10);
1146 
1147             n = wallTime.getMonthDay();
1148             buf[6] = toChar(n / 10);
1149             buf[7] = toChar(n % 10);
1150 
1151             if (!hasTime) {
1152                 return new String(buf, 0, 8);
1153             }
1154 
1155             buf[8] = 'T';
1156 
1157             n = wallTime.getHour();
1158             buf[9] = toChar(n / 10);
1159             buf[10] = toChar(n % 10);
1160 
1161             n = wallTime.getMinute();
1162             buf[11] = toChar(n / 10);
1163             buf[12] = toChar(n % 10);
1164 
1165             n = wallTime.getSecond();
1166             buf[13] = toChar(n / 10);
1167             buf[14] = toChar(n % 10);
1168 
1169             if (TIMEZONE_UTC.equals(timezone)) {
1170                 // The letter 'Z' is appended to the end.
1171                 buf[15] = 'Z';
1172                 return new String(buf, 0, 16);
1173             } else {
1174                 return new String(buf, 0, 15);
1175             }
1176         }
1177 
toChar(int n)1178         private char toChar(int n) {
1179             return (n >= 0 && n <= 9) ? (char) (n + '0') : ' ';
1180         }
1181 
1182         /**
1183          * A method that will return the state of this object in string form. Note: it has side
1184          * effects and so has deliberately not been made the default {@link #toString()}.
1185          */
toStringInternal()1186         public String toStringInternal() {
1187             // This implementation possibly displays the un-normalized fields because that is
1188             // what it has always done.
1189             return String.format("%04d%02d%02dT%02d%02d%02d%s(%d,%d,%d,%d,%d)",
1190                     wallTime.getYear(),
1191                     wallTime.getMonth() + 1,
1192                     wallTime.getMonthDay(),
1193                     wallTime.getHour(),
1194                     wallTime.getMinute(),
1195                     wallTime.getSecond(),
1196                     timezone,
1197                     wallTime.getWeekDay(),
1198                     wallTime.getYearDay(),
1199                     wallTime.getGmtOffset(),
1200                     wallTime.getIsDst(),
1201                     toMillis(false /* use isDst */) / 1000
1202             );
1203 
1204         }
1205 
compare(TimeCalculator aObject, TimeCalculator bObject)1206         public static int compare(TimeCalculator aObject, TimeCalculator bObject) {
1207             if (aObject.timezone.equals(bObject.timezone)) {
1208                 // If the timezones are the same, we can easily compare the two times.
1209                 int diff = aObject.wallTime.getYear() - bObject.wallTime.getYear();
1210                 if (diff != 0) {
1211                     return diff;
1212                 }
1213 
1214                 diff = aObject.wallTime.getMonth() - bObject.wallTime.getMonth();
1215                 if (diff != 0) {
1216                     return diff;
1217                 }
1218 
1219                 diff = aObject.wallTime.getMonthDay() - bObject.wallTime.getMonthDay();
1220                 if (diff != 0) {
1221                     return diff;
1222                 }
1223 
1224                 diff = aObject.wallTime.getHour() - bObject.wallTime.getHour();
1225                 if (diff != 0) {
1226                     return diff;
1227                 }
1228 
1229                 diff = aObject.wallTime.getMinute() - bObject.wallTime.getMinute();
1230                 if (diff != 0) {
1231                     return diff;
1232                 }
1233 
1234                 diff = aObject.wallTime.getSecond() - bObject.wallTime.getSecond();
1235                 if (diff != 0) {
1236                     return diff;
1237                 }
1238 
1239                 return 0;
1240             } else {
1241                 // Otherwise, convert to milliseconds and compare that. This requires that object be
1242                 // normalized. Note: For dates that do not exist: toMillis() can return -1, which
1243                 // can be confused with a valid time.
1244                 long am = aObject.toMillis(false /* use isDst */);
1245                 long bm = bObject.toMillis(false /* use isDst */);
1246                 long diff = am - bm;
1247                 return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0);
1248             }
1249 
1250         }
1251 
copyFieldsToTime(Time time)1252         public void copyFieldsToTime(Time time) {
1253             time.second = wallTime.getSecond();
1254             time.minute = wallTime.getMinute();
1255             time.hour = wallTime.getHour();
1256             time.monthDay = wallTime.getMonthDay();
1257             time.month = wallTime.getMonth();
1258             time.year = wallTime.getYear();
1259 
1260             // Read-only fields that are derived from other information above.
1261             time.weekDay = wallTime.getWeekDay();
1262             time.yearDay = wallTime.getYearDay();
1263 
1264             // < 0: DST status unknown, 0: is not in DST, 1: is in DST
1265             time.isDst = wallTime.getIsDst();
1266             // This is in seconds and includes any DST offset too.
1267             time.gmtoff = wallTime.getGmtOffset();
1268         }
1269 
copyFieldsFromTime(Time time)1270         public void copyFieldsFromTime(Time time) {
1271             wallTime.setSecond(time.second);
1272             wallTime.setMinute(time.minute);
1273             wallTime.setHour(time.hour);
1274             wallTime.setMonthDay(time.monthDay);
1275             wallTime.setMonth(time.month);
1276             wallTime.setYear(time.year);
1277             wallTime.setWeekDay(time.weekDay);
1278             wallTime.setYearDay(time.yearDay);
1279             wallTime.setIsDst(time.isDst);
1280             wallTime.setGmtOffset((int) time.gmtoff);
1281 
1282             if (time.allDay && (time.second != 0 || time.minute != 0 || time.hour != 0)) {
1283                 throw new IllegalArgumentException("allDay is true but sec, min, hour are not 0.");
1284             }
1285 
1286             timezone = time.timezone;
1287             updateZoneInfoFromTimeZone();
1288         }
1289     }
1290 }
1291