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