Home | History | Annotate | Download | only in format
      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.util.ZoneInfo;
     22 import libcore.util.ZoneInfoDB;
     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      */
    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      */
    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      */
    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. */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    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 
    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 
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
   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      */
   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 
   1069         public TimeCalculator(String timezoneId) {
   1070             this.zoneInfo = lookupZoneInfo(timezoneId);
   1071             this.wallTime = new ZoneInfo.WallTime();
   1072         }
   1073 
   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 
   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 
   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 
   1102         private void updateZoneInfoFromTimeZone() {
   1103             if (!zoneInfo.getID().equals(timezone)) {
   1104                 this.zoneInfo = lookupZoneInfo(timezone);
   1105             }
   1106         }
   1107 
   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 
   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 
   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 
   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          */
   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 
   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 
   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 
   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