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