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