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 java.io.IOException; 22 import java.util.Locale; 23 import java.util.TimeZone; 24 25 import libcore.util.ZoneInfo; 26 import 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 56 public 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 */ Time(String timezoneId)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 */ Time()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 */ Time(Time other)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. */ initialize(String timezoneId)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 */ normalize(boolean ignoreDst)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 */ switchTimezone(String timezone)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 */ getActualMaximum(int field)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 */ clear(String timezoneId)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 */ compare(Time a, Time b)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 */ format(String format)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 toString()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 */ parse(String s)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 */ parseInternal(String s)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 checkChar(String s, int spos, char expected)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 getChar(String s, int spos, int mul)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 */ parse3339(String s)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 parse3339Internal(String s)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 */ getCurrentTimezone()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 */ setToNow()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 */ toMillis(boolean ignoreDst)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 */ set(long millis)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 */ format2445()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 */ set(Time that)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 */ set(int second, int minute, int hour, int monthDay, int month, int year)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 */ set(int monthDay, int month, int year)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 */ before(Time that)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 */ after(Time that)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 */ getWeekNumber()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 */ format3339(boolean allDay)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 */ isEpoch(Time time)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 */ getJulianDay(long millis, long gmtoff)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 */ setJulianDay(int julianDay)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 */ getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek)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 */ getJulianMondayFromWeeksSinceEpoch(int week)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 TimeCalculator(String timezoneId)1070 public TimeCalculator(String timezoneId) { 1071 this.zoneInfo = lookupZoneInfo(timezoneId); 1072 this.wallTime = new ZoneInfo.WallTime(); 1073 } 1074 toMillis(boolean ignoreDst)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 setTimeInMillis(long millis)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 format(String format)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 updateZoneInfoFromTimeZone()1103 private void updateZoneInfoFromTimeZone() { 1104 if (!zoneInfo.getID().equals(timezone)) { 1105 this.zoneInfo = lookupZoneInfo(timezone); 1106 } 1107 } 1108 lookupZoneInfo(String timezoneId)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 switchTimeZone(String timezone)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 format2445(boolean hasTime)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 toChar(int n)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 */ toStringInternal()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 compare(TimeCalculator aObject, TimeCalculator bObject)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 copyFieldsToTime(Time time)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 copyFieldsFromTime(Time time)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