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