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