Time.java revision 3b852e3489e995600fce19dfdbf3a5d769374d74
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
17package android.text.format;
18
19import android.util.TimeFormatException;
20
21import java.io.IOException;
22import java.util.Locale;
23import java.util.TimeZone;
24
25import libcore.util.ZoneInfo;
26import 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 */
52public 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