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