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