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